From c547c2301f173a2590ec26772a4af8a3642d4fca Mon Sep 17 00:00:00 2001 From: NigeParis Date: Fri, 16 Jan 2026 15:24:13 +0100 Subject: [PATCH 01/10] button friend in profile --- frontend/src/chat/chat.css | 7 +++++++ frontend/src/chat/chatHelperFunctions/openProfilePopup.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/chat/chat.css b/frontend/src/chat/chat.css index 1555807..64d64bb 100644 --- a/frontend/src/chat/chat.css +++ b/frontend/src/chat/chat.css @@ -291,6 +291,13 @@ div-private { right-12 } +.popup-b-friend { + @apply + absolute + bottom-32 + left-12 +} + .popup-b-game { @apply absolute diff --git a/frontend/src/chat/chatHelperFunctions/openProfilePopup.ts b/frontend/src/chat/chatHelperFunctions/openProfilePopup.ts index fcef7e9..34a7165 100644 --- a/frontend/src/chat/chatHelperFunctions/openProfilePopup.ts +++ b/frontend/src/chat/chatHelperFunctions/openProfilePopup.ts @@ -6,7 +6,7 @@ export async function openProfilePopup(profil: ClientProfil) { modalname.innerHTML = `
- Profile of ${profil.user}
+ Profile of ${profil.user} Login status: ${profil.loginName ?? 'Guest'}
Login ID: ${profil.userID ?? ''} @@ -16,7 +16,7 @@ export async function openProfilePopup(profil: ClientProfil) {
About: ${profil.text}
- + `; const profilList = document.getElementById("profile-modal") ?? null; From 95784c9719e858fde6c8d1c2353e15381add5848 Mon Sep 17 00:00:00 2001 From: NigeParis Date: Fri, 16 Jan 2026 15:47:25 +0100 Subject: [PATCH 02/10] Added function to buttun friend actionBtnFriend --- frontend/src/chat/chat.ts | 2 ++ .../chatHelperFunctions/actionBtnFriend.ts | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 frontend/src/chat/chatHelperFunctions/actionBtnFriend.ts diff --git a/frontend/src/chat/chat.ts b/frontend/src/chat/chat.ts index e0db9f5..f3e2aa2 100644 --- a/frontend/src/chat/chat.ts +++ b/frontend/src/chat/chat.ts @@ -29,6 +29,7 @@ import { windowStateVisable } from "./chatHelperFunctions/windowStateVisable"; import { cmdList } from "./chatHelperFunctions/cmdList"; import { actionBtnTTTGames } from "./chatHelperFunctions/actionBtnTTTGames"; import { showError } from "@app/toast"; +import { actionBtnFriend } from "./chatHelperFunctions/actionBtnFriend"; const MAX_SYSTEM_MESSAGES = 10; let inviteMsgFlag: boolean = false; @@ -256,6 +257,7 @@ function initChatSocket() { actionBtnPopUpBlock(profil, socket); actionBtnPongGames(profil, socket); actionBtnTTTGames(profil, socket); + actionBtnFriend(profil, socket); }); socket.on("blockUser", (blocked: ClientProfil) => { diff --git a/frontend/src/chat/chatHelperFunctions/actionBtnFriend.ts b/frontend/src/chat/chatHelperFunctions/actionBtnFriend.ts new file mode 100644 index 0000000..c804154 --- /dev/null +++ b/frontend/src/chat/chatHelperFunctions/actionBtnFriend.ts @@ -0,0 +1,27 @@ +import type { ClientProfil } from "../types_front"; +import { Socket } from "socket.io-client"; + +/** + * function listens for a click on the TTT game History button + * @param profile - Clients target profil + * @param senderSocket - socket from the sender +**/ + +export function actionBtnFriend(profile: ClientProfil, senderSocket: Socket) { + setTimeout(() => { + const friend = document.querySelector("#btn-friend"); + friend?.addEventListener("click", () => { + + if (friend.textContent = "friend") { + friend.textContent = "not-friend" + console.log('friend'); + } + else { + friend.textContent = "not-friend" + console.log('Not a friend'); + + } + + }); + }, 0) +}; \ No newline at end of file From 3cccc18e9a50a6f9c1000005e347f1cd3fdab017 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Fri, 16 Jan 2026 13:34:51 +0100 Subject: [PATCH 03/10] icons started to work yeah --- .gitignore | 4 + Docker.mk | 3 +- docker-compose.yml | 36 ++- nginx/conf/locations/icons.conf | 9 + src/@dockerfiles/deps.Dockerfile | 1 + src/auth/config/default.png | Bin 0 -> 22473 bytes src/auth/src/routes/guestLogin.ts | 2 + src/auth/src/routes/oauth2/callback.ts | 4 + src/auth/src/routes/signin.ts | 2 + src/icons/.dockerignore | 2 + src/icons/package.json | 35 +++ src/icons/src/app.ts | 33 ++ src/icons/src/openapi.ts | 21 ++ src/icons/src/plugins/README.md | 16 + src/icons/src/routes/set.ts | 72 +++++ src/icons/src/run.ts | 21 ++ src/icons/tsconfig.json | 5 + src/icons/vite.config.js | 54 ++++ src/pnpm-lock.yaml | 406 +++++++++++++++++++++++++ src/pnpm-workspace.yaml | 1 + 20 files changed, 719 insertions(+), 8 deletions(-) create mode 100644 nginx/conf/locations/icons.conf create mode 100644 src/auth/config/default.png create mode 100644 src/icons/.dockerignore create mode 100644 src/icons/package.json create mode 100644 src/icons/src/app.ts create mode 100644 src/icons/src/openapi.ts create mode 100644 src/icons/src/plugins/README.md create mode 100644 src/icons/src/routes/set.ts create mode 100644 src/icons/src/run.ts create mode 100644 src/icons/tsconfig.json create mode 100644 src/icons/vite.config.js diff --git a/.gitignore b/.gitignore index 1ec2f88..195310c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ pnpm-lock.yaml package-lock.json .idea .dev +openapi-template +nginx-dev +src/redocly.yaml +flake/ diff --git a/Docker.mk b/Docker.mk index 6e5bec6..3bc0a48 100644 --- a/Docker.mk +++ b/Docker.mk @@ -34,7 +34,8 @@ ifeq "$(REDUCED_SET)" "y" tic-tac-toe \ nginx \ user \ - pong + pong \ + icons endif all: build diff --git a/docker-compose.yml b/docker-compose.yml index 6f5362b..a1a7f26 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,7 +50,7 @@ services: - '9090:8443' volumes: # if you need to share files with nginx, you do it here. - - static-volume:/volumes/static + - icons-volume:/volumes/icons env_file: .env logging: driver: gelf @@ -72,7 +72,7 @@ services: - app volumes: - sqlite-volume:/volumes/database - - static-volume:/volumes/static + - icons-volume:/volumes/icons - ./src/auth/config:/config env_file: .env logging: @@ -98,7 +98,32 @@ services: - app volumes: - sqlite-volume:/volumes/database - - static-volume:/volumes/static + env_file: .env + logging: + driver: gelf + options: + gelf-address: "udp://127.0.0.1:12201" + tag: "{{.Name}}" + + + ############### + # ICONS # + ############### + icons: + build: + context: ./src/ + args: + - SERVICE=icons + additional_contexts: + pnpm_base: "service:pnpm_base" + pnpm_deps: "service:pnpm_deps" + container_name: app-icons + restart: always + networks: + - app + volumes: + - sqlite-volume:/volumes/database + - icons-volume:/volumes/icons env_file: .env logging: driver: gelf @@ -124,7 +149,6 @@ services: env_file: .env volumes: - sqlite-volume:/volumes/database - - static-volume:/volumes/static logging: driver: gelf options: @@ -148,7 +172,6 @@ services: - app volumes: - sqlite-volume:/volumes/database - - static-volume:/volumes/static env_file: .env logging: driver: gelf @@ -173,7 +196,6 @@ services: - app volumes: - sqlite-volume:/volumes/database - - static-volume:/volumes/static env_file: .env logging: driver: gelf @@ -358,6 +380,6 @@ services: volumes: sqlite-volume: - static-volume: + icons-volume: grafana-data: elastic-data: diff --git a/nginx/conf/locations/icons.conf b/nginx/conf/locations/icons.conf new file mode 100644 index 0000000..386664d --- /dev/null +++ b/nginx/conf/locations/icons.conf @@ -0,0 +1,9 @@ +#forward the post request to the microservice +location /api/icons/ { + proxy_pass http://app-icons; +} + +location /icons/ { + root /volumes/; + default_type image/png; +} diff --git a/src/@dockerfiles/deps.Dockerfile b/src/@dockerfiles/deps.Dockerfile index df81482..8409f9b 100644 --- a/src/@dockerfiles/deps.Dockerfile +++ b/src/@dockerfiles/deps.Dockerfile @@ -10,5 +10,6 @@ COPY auth/package.json /build/auth/package.json COPY chat/package.json /build/chat/package.json COPY tic-tac-toe/package.json /build/tic-tac-toe/package.json COPY user/package.json /build/user/package.json +COPY icons/package.json /build/icons/package.json RUN pnpm install -q --frozen-lockfile; diff --git a/src/auth/config/default.png b/src/auth/config/default.png new file mode 100644 index 0000000000000000000000000000000000000000..bb9af0faca17e6618c62696fab5742af96ffe68c GIT binary patch literal 22473 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 => { 'guestLogin.failed.generic.unknown', ); } + await fs.cp('/config/default.png', `/volumes/icons/${user.id}`); return res.makeResponse(200, 'success', 'guestLogin.success', { token: this.signJwt('auth', user.id.toString()), }); diff --git a/src/auth/src/routes/oauth2/callback.ts b/src/auth/src/routes/oauth2/callback.ts index 36ec4a3..1a3689a 100644 --- a/src/auth/src/routes/oauth2/callback.ts +++ b/src/auth/src/routes/oauth2/callback.ts @@ -3,6 +3,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Static, Type } from 'typebox'; import { typeResponse, isNullish } from '@shared/utils'; import * as oauth2 from '../../oauth2'; +import * as fs from 'node:fs/promises'; export const WhoAmIRes = Type.Union([ @@ -47,6 +48,9 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { user_name = `${orig}${Date.now()}`; } u = await this.db.createOauth2User(user_name, provider.display_name, userinfo.unique_id); + if (u) { + await fs.cp('/config/default.png', `/volumes/icons/${u.id}`); + } } if (isNullish(u)) { return res.code(500).send('failed to fetch or create user...'); diff --git a/src/auth/src/routes/signin.ts b/src/auth/src/routes/signin.ts index fe09163..094c8f1 100644 --- a/src/auth/src/routes/signin.ts +++ b/src/auth/src/routes/signin.ts @@ -2,6 +2,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Static, Type } from 'typebox'; import { typeResponse, isNullish, MakeStaticResponse } from '@shared/utils'; +import * as fs from 'node:fs/promises'; const USERNAME_CHECK: RegExp = /^[a-zA-Z_0-9]+$/; @@ -61,6 +62,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { } const u = await this.db.createUser(name, user_name, password); if (isNullish(u)) { return res.makeResponse(500, 'failed', 'signin.failed.generic'); } + await fs.cp('/config/default.png', `/volumes/icons/${u.id}`); // every check has been passed, they are now logged in, using this token to say who they are... const userToken = this.signJwt('auth', u.id); diff --git a/src/icons/.dockerignore b/src/icons/.dockerignore new file mode 100644 index 0000000..c925c21 --- /dev/null +++ b/src/icons/.dockerignore @@ -0,0 +1,2 @@ +/dist +/node_modules diff --git a/src/icons/package.json b/src/icons/package.json new file mode 100644 index 0000000..09922ab --- /dev/null +++ b/src/icons/package.json @@ -0,0 +1,35 @@ +{ + "type": "module", + "private": false, + "name": "icons", + "version": "1.0.0", + "description": "This project was bootstrapped with Fastify-CLI.", + "main": "app.ts", + "directories": { + "test": "test" + }, + "scripts": { + "start": "npm run build && node dist/run.js", + "build": "vite build", + "build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false", + "build:openapi": "VITE_ENTRYPOINT=src/openapi.ts vite build && node dist/openapi.cjs >openapi.json" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@fastify/formbody": "^8.0.2", + "@fastify/multipart": "^9.3.0", + "fastify": "^5.6.2", + "fastify-plugin": "^5.1.0", + "file-type": "^21.3.0", + "sharp": "^0.34.5", + "typebox": "^1.0.69" + }, + "devDependencies": { + "@types/node": "^22.19.3", + "rollup-plugin-node-externals": "^8.1.2", + "vite": "^7.3.0", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/src/icons/src/app.ts b/src/icons/src/app.ts new file mode 100644 index 0000000..07d3939 --- /dev/null +++ b/src/icons/src/app.ts @@ -0,0 +1,33 @@ +import { FastifyPluginAsync } from 'fastify'; +import * as db from '@shared/database'; +import * as auth from '@shared/auth'; +import * as swagger from '@shared/swagger'; +import * as utils from '@shared/utils'; + +declare const __SERVICE_NAME: string; + +// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this... +const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true }); +// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this... +const routes = import.meta.glob('./routes/**/*.ts', { eager: true }); + +const app: FastifyPluginAsync = async (fastify, opts): Promise => { + void opts; + await fastify.register(utils.useMakeResponse); + await fastify.register(utils.useMonitoring); + await fastify.register(swagger.useSwagger, { service: __SERVICE_NAME }); + await fastify.register(db.useDatabase as FastifyPluginAsync, {}); + await fastify.register(auth.jwtPlugin as FastifyPluginAsync, {}); + await fastify.register(auth.authPlugin as FastifyPluginAsync, {}); + + // Place here your custom code! + for (const plugin of Object.values(plugins)) { + void fastify.register(plugin as FastifyPluginAsync, {}); + } + for (const route of Object.values(routes)) { + void fastify.register(route as FastifyPluginAsync, {}); + } +}; + +export default app; +export { app }; diff --git a/src/icons/src/openapi.ts b/src/icons/src/openapi.ts new file mode 100644 index 0000000..d66d7a7 --- /dev/null +++ b/src/icons/src/openapi.ts @@ -0,0 +1,21 @@ +import f, { FastifyPluginAsync } from 'fastify'; +import * as swagger from '@shared/swagger'; +import * as auth from '@shared/auth'; + +declare const __SERVICE_NAME: string; + +// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this... +const routes = import.meta.glob('./routes/**/*.ts', { eager: true }); + +async function start() { + const fastify = f({ logger: false }); + await fastify.register(auth.authPlugin, { onlySchema: true }); + await fastify.register(swagger.useSwagger, { service: __SERVICE_NAME }); + + for (const route of Object.values(routes)) { + await fastify.register(route as FastifyPluginAsync, {}); + } + await fastify.ready(); + console.log(JSON.stringify(fastify.swagger(), undefined, 4)); +} +start(); diff --git a/src/icons/src/plugins/README.md b/src/icons/src/plugins/README.md new file mode 100644 index 0000000..1e61ee5 --- /dev/null +++ b/src/icons/src/plugins/README.md @@ -0,0 +1,16 @@ +# Plugins Folder + +Plugins define behavior that is common to all the routes in your +application. Authentication, caching, templates, and all the other cross +cutting concerns should be handled by plugins placed in this folder. + +Files in this folder are typically defined through the +[`fastify-plugin`](https://github.com/fastify/fastify-plugin) module, +making them non-encapsulated. They can define decorators and set hooks +that will then be used in the rest of your application. + +Check out: + +* [The hitchhiker's guide to plugins](https://fastify.dev/docs/latest/Guides/Plugins-Guide/) +* [Fastify decorators](https://fastify.dev/docs/latest/Reference/Decorators/). +* [Fastify lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/). diff --git a/src/icons/src/routes/set.ts b/src/icons/src/routes/set.ts new file mode 100644 index 0000000..5837a6c --- /dev/null +++ b/src/icons/src/routes/set.ts @@ -0,0 +1,72 @@ +import { FastifyPluginAsync } from 'fastify'; +import multipart from '@fastify/multipart'; +import { MakeStaticResponse, typeResponse } from '@shared/utils'; +import { fileTypeFromBuffer } from 'file-type'; +import sharp from 'sharp'; +import path from 'path'; +import fs from 'node:fs/promises'; + + +export const IconSetRes = { + '200': typeResponse('success', 'iconset.success'), + '400': typeResponse('success', ['iconset.failure.invalidFile', 'iconset.failure.noFile']), +}; + +export type IconSetRes = MakeStaticResponse; + +const validMimeTypes = new Set([ + 'image/jpeg', + 'image/png', +]); + +async function resizeAndSaveImage( + imageBuffer: Buffer, + filename: string, +): Promise { + const outputDir = '/volumes/icons/'; + const outputPath = path.join(outputDir, filename); + + // Ensure the directory exists + await fs.mkdir(outputDir, { recursive: true }); + + await sharp(imageBuffer) + .resize(512, 512, { + fit: 'cover', + }) + .png() + .toFile(outputPath); +} + +const route: FastifyPluginAsync = async (fastify, _opts): Promise => { + void _opts; + await fastify.register(multipart); + fastify.post( + '/api/icons/set', + { schema: { response: IconSetRes, operationId: 'setIcons' }, config: { requireAuth: true } }, + async function(req, res) { + // req.authUser is always set, since this is gated + const userid = req.authUser!.id; + const file = await req.file(); + if (!file) { + return res.makeResponse(400, 'failure', 'iconset.failure.noFile'); + } + if (!validMimeTypes.has(file.mimetype)) { + return res.makeResponse(400, 'failure', 'iconset.failure.invalidFile'); + } + const buf = await file.toBuffer(); + if (!validMimeTypes.has((await fileTypeFromBuffer(buf))?.mime ?? 'unknown')) { + return res.makeResponse(400, 'failure', 'iconset.failure.invalidFile'); + } + try { + resizeAndSaveImage(buf, userid); + return res.makeResponse(200, 'success', 'iconset.success'); + } + catch (e: unknown) { + this.log.warn(e); + return res.makeResponse(400, 'failure', 'iconset.failure.invalidFile'); + } + }, + ); +}; + +export default route; diff --git a/src/icons/src/run.ts b/src/icons/src/run.ts new file mode 100644 index 0000000..3c59d5d --- /dev/null +++ b/src/icons/src/run.ts @@ -0,0 +1,21 @@ +// this sould only be used by the docker file ! + +import fastify, { FastifyInstance } from 'fastify'; +import app from './app'; + +const start = async () => { + const f: FastifyInstance = fastify({ logger: { level: 'info' } }); + process.on('SIGTERM', () => { + f.log.warn('Requested to shutdown'); + process.exit(134); + }); + try { + await f.register(app, {}); + await f.listen({ port: 80, host: '0.0.0.0' }); + } + catch (err) { + f.log.error(err); + process.exit(1); + } +}; +start(); diff --git a/src/icons/tsconfig.json b/src/icons/tsconfig.json new file mode 100644 index 0000000..e6d24e2 --- /dev/null +++ b/src/icons/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} diff --git a/src/icons/vite.config.js b/src/icons/vite.config.js new file mode 100644 index 0000000..aa3ef08 --- /dev/null +++ b/src/icons/vite.config.js @@ -0,0 +1,54 @@ +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import nodeExternals from 'rollup-plugin-node-externals'; +import path from 'node:path'; +import fs from 'node:fs'; + +function collectDeps(...pkgJsonPaths) { + const allDeps = new Set(); + for (const pkgPath of pkgJsonPaths) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + for (const dep of Object.keys(pkg.dependencies || {})) { + allDeps.add(dep); + } + for (const peer of Object.keys(pkg.peerDependencies || {})) { + allDeps.add(peer); + } + } + return Array.from(allDeps); +} + +const externals = collectDeps( + './package.json', + '../@shared/package.json', +); + + +export default defineConfig({ + root: __dirname, + define: { + __SERVICE_NAME: '"icons"', + }, + // service root + plugins: [tsconfigPaths(), nodeExternals()], + build: { + ssr: true, + outDir: 'dist', + emptyOutDir: true, + lib: { + entry: path.resolve(__dirname, process.env.VITE_ENTRYPOINT ?? 'src/run.ts'), + // adjust main entry + formats: ['cjs'], + // CommonJS for Node.js + fileName: () => 'index.js', + }, + rollupOptions: { + external: externals, + }, + target: 'node22', + // or whatever Node version you use + sourcemap: true, + minify: false, + // for easier debugging + }, +}); diff --git a/src/pnpm-lock.yaml b/src/pnpm-lock.yaml index 0268c48..9571c8c 100644 --- a/src/pnpm-lock.yaml +++ b/src/pnpm-lock.yaml @@ -141,6 +141,43 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@22.19.3)(yaml@2.8.2)) + icons: + dependencies: + '@fastify/formbody': + specifier: ^8.0.2 + version: 8.0.2 + '@fastify/multipart': + specifier: ^9.3.0 + version: 9.3.0 + fastify: + specifier: ^5.6.2 + version: 5.6.2 + fastify-plugin: + specifier: ^5.1.0 + version: 5.1.0 + file-type: + specifier: ^21.3.0 + version: 21.3.0 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + typebox: + specifier: ^1.0.69 + version: 1.0.69 + devDependencies: + '@types/node': + specifier: ^22.19.3 + version: 22.19.3 + rollup-plugin-node-externals: + specifier: ^8.1.2 + version: 8.1.2(rollup@4.54.0) + vite: + specifier: ^7.3.0 + version: 7.3.0(@types/node@22.19.3)(yaml@2.8.2) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@22.19.3)(yaml@2.8.2)) + pong: dependencies: fastify: @@ -224,6 +261,12 @@ importers: packages: + '@borewit/text-codec@0.2.1': + resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} @@ -424,15 +467,24 @@ packages: '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@fastify/cookie@11.0.2': resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} + '@fastify/deepmerge@3.1.0': + resolution: {integrity: sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} '@fastify/fast-json-stringify-compiler@5.0.3': resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + '@fastify/formbody@8.0.2': + resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==} + '@fastify/forwarded@3.0.1': resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} @@ -442,6 +494,9 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + '@fastify/multipart@9.3.0': + resolution: {integrity: sha512-NpeKipTOjjL1dA7SSlRMrOWWtrE8/0yKOmeudkdQoEaz4sVDJw5MVdZIahsWhvpc3YTN7f04f9ep/Y65RKoOWA==} + '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} @@ -473,6 +528,143 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -605,6 +797,13 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@types/bcrypt@6.0.0': resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} @@ -1042,6 +1241,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@21.3.0: + resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} + engines: {node: '>=20'} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -1497,6 +1700,10 @@ packages: sha1@1.1.1: resolution: {integrity: sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1587,6 +1794,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1617,6 +1828,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + ts-api-utils@2.3.0: resolution: {integrity: sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==} engines: {node: '>=18.12'} @@ -1633,6 +1848,9 @@ packages: typescript: optional: true + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -1655,6 +1873,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -1774,6 +1996,13 @@ packages: snapshots: + '@borewit/text-codec@0.2.1': {} + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.2': optional: true @@ -1906,17 +2135,26 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.1.0 + '@fastify/busboy@3.2.0': {} + '@fastify/cookie@11.0.2': dependencies: cookie: 1.1.1 fastify-plugin: 5.1.0 + '@fastify/deepmerge@3.1.0': {} + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.0.3': dependencies: fast-json-stringify: 6.1.1 + '@fastify/formbody@8.0.2': + dependencies: + fast-querystring: 1.1.2 + fastify-plugin: 5.1.0 + '@fastify/forwarded@3.0.1': {} '@fastify/jwt@9.1.0': @@ -1931,6 +2169,14 @@ snapshots: dependencies: dequal: 2.0.3 + '@fastify/multipart@9.3.0': + dependencies: + '@fastify/busboy': 3.2.0 + '@fastify/deepmerge': 3.1.0 + '@fastify/error': 4.2.0 + fastify-plugin: 5.1.0 + secure-json-parse: 4.1.0 + '@fastify/proxy-addr@5.1.0': dependencies: '@fastify/forwarded': 3.0.1 @@ -1982,6 +2228,102 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -2069,6 +2411,15 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + '@types/bcrypt@6.0.0': dependencies: '@types/node': 22.19.3 @@ -2584,6 +2935,15 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@21.3.0: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.4 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -3026,6 +3386,37 @@ snapshots: charenc: 0.0.2 crypt: 0.0.2 + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3136,6 +3527,10 @@ snapshots: strip-json-comments@3.1.1: {} + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3172,6 +3567,12 @@ snapshots: toidentifier@1.0.1: {} + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.1 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + ts-api-utils@2.3.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -3180,6 +3581,9 @@ snapshots: optionalDependencies: typescript: 5.9.3 + tslib@2.8.1: + optional: true + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -3203,6 +3607,8 @@ snapshots: typescript@5.9.3: {} + uint8array-extras@1.5.0: {} + undici-types@6.21.0: {} undici-types@7.16.0: diff --git a/src/pnpm-workspace.yaml b/src/pnpm-workspace.yaml index 311c660..2840480 100644 --- a/src/pnpm-workspace.yaml +++ b/src/pnpm-workspace.yaml @@ -9,3 +9,4 @@ onlyBuiltDependencies: - core-js - esbuild - protobufjs + - sharp From 5884407f35add6de73579ac5972b9a558bbbc24c Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Fri, 16 Jan 2026 15:46:50 +0100 Subject: [PATCH 04/10] feat(frontend): you can now update your icon --- frontend/src/pages/profile/profile.html | 16 ++++++- frontend/src/pages/profile/profile.ts | 58 +++++++++++++++++++++---- nginx/conf/locations/icons.conf | 1 + 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/profile/profile.html b/frontend/src/pages/profile/profile.html index 5dfa4f2..b24ab6d 100644 --- a/frontend/src/pages/profile/profile.html +++ b/frontend/src/pages/profile/profile.html @@ -31,6 +31,19 @@ + +
@@ -52,8 +65,7 @@ - +
Login status: ${profil.loginName ?? 'Guest'}
- Login ID: ${profil.userID ?? ''} + Login ID: ${profil.userID ?? ''} +
From 590604b385918034891587f022ca83137d59406a Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Fri, 16 Jan 2026 16:18:54 +0100 Subject: [PATCH 06/10] friends-backend --- .../api/generated/.openapi-generator/FILES | 8 +- .../src/api/generated/apis/OpenapiOtherApi.ts | 173 ++++++++++ .../generated/models/AddFriend200Response.ts | 93 +++++ .../generated/models/AddFriend404Response.ts | 93 +++++ .../generated/models/ListFriend200Response.ts | 110 ++++++ .../models/ListFriend200ResponsePayload.ts | 74 ++++ ...istFriend200ResponsePayloadFriendsInner.ts | 75 ++++ .../generated/models/ListFriend404Response.ts | 93 +++++ .../models/RemoveFriend200Response.ts | 93 +++++ .../models/RemoveFriend404Response.ts | 93 +++++ .../TttHistory200ResponsePayloadDataInner.ts | 28 +- frontend/src/api/generated/models/index.ts | 8 +- src/@shared/src/database/index.ts | 4 +- src/@shared/src/database/init.sql | 9 + src/@shared/src/database/mixin/friends.ts | 72 ++++ src/icons/openapi.json | 21 ++ src/icons/src/routes/set.ts | 49 ++- src/openapi.json | 320 ++++++++++++++++++ src/user/openapi.json | 311 +++++++++++++++++ src/user/src/routes/friendAdd.ts | 44 +++ src/user/src/routes/friendList.ts | 40 +++ src/user/src/routes/friendRemove.ts | 44 +++ 22 files changed, 1826 insertions(+), 29 deletions(-) create mode 100644 frontend/src/api/generated/models/AddFriend200Response.ts create mode 100644 frontend/src/api/generated/models/AddFriend404Response.ts create mode 100644 frontend/src/api/generated/models/ListFriend200Response.ts create mode 100644 frontend/src/api/generated/models/ListFriend200ResponsePayload.ts create mode 100644 frontend/src/api/generated/models/ListFriend200ResponsePayloadFriendsInner.ts create mode 100644 frontend/src/api/generated/models/ListFriend404Response.ts create mode 100644 frontend/src/api/generated/models/RemoveFriend200Response.ts create mode 100644 frontend/src/api/generated/models/RemoveFriend404Response.ts create mode 100644 src/@shared/src/database/mixin/friends.ts create mode 100644 src/icons/openapi.json create mode 100644 src/user/src/routes/friendAdd.ts create mode 100644 src/user/src/routes/friendList.ts create mode 100644 src/user/src/routes/friendRemove.ts diff --git a/frontend/src/api/generated/.openapi-generator/FILES b/frontend/src/api/generated/.openapi-generator/FILES index 0a4207f..dd4bc5c 100644 --- a/frontend/src/api/generated/.openapi-generator/FILES +++ b/frontend/src/api/generated/.openapi-generator/FILES @@ -1,6 +1,8 @@ apis/OpenapiOtherApi.ts apis/index.ts index.ts +models/AddFriend200Response.ts +models/AddFriend404Response.ts models/AllowGuestMessage200Response.ts models/AllowGuestMessage403Response.ts models/ChangeDesc200Response.ts @@ -38,6 +40,9 @@ models/GuestLogin200ResponsePayload.ts models/GuestLogin400Response.ts models/GuestLogin500Response.ts models/GuestLoginRequest.ts +models/ListFriend200Response.ts +models/ListFriend200ResponsePayload.ts +models/ListFriend200ResponsePayloadFriendsInner.ts models/Login200Response.ts models/Login202Response.ts models/Login202ResponsePayload.ts @@ -60,6 +65,8 @@ models/ProviderList200Response.ts models/ProviderList200ResponsePayload.ts models/ProviderList200ResponsePayloadListInner.ts models/ProviderList200ResponsePayloadListInnerColors.ts +models/RemoveFriend200Response.ts +models/RemoveFriend404Response.ts models/Signin200Response.ts models/Signin200ResponsePayload.ts models/Signin400Response.ts @@ -82,7 +89,6 @@ models/TournamentList404Response.ts models/TttHistory200Response.ts models/TttHistory200ResponsePayload.ts models/TttHistory200ResponsePayloadDataInner.ts -models/TttHistory200ResponsePayloadDataInnerPlayerX.ts models/TttHistory404Response.ts models/index.ts runtime.ts diff --git a/frontend/src/api/generated/apis/OpenapiOtherApi.ts b/frontend/src/api/generated/apis/OpenapiOtherApi.ts index 84da0d5..0ca170c 100644 --- a/frontend/src/api/generated/apis/OpenapiOtherApi.ts +++ b/frontend/src/api/generated/apis/OpenapiOtherApi.ts @@ -15,6 +15,8 @@ import * as runtime from '../runtime'; import type { + AddFriend200Response, + AddFriend404Response, AllowGuestMessage200Response, AllowGuestMessage403Response, ChangeDesc200Response, @@ -46,6 +48,7 @@ import type { GuestLogin400Response, GuestLogin500Response, GuestLoginRequest, + ListFriend200Response, Login200Response, Login202Response, Login400Response, @@ -60,6 +63,8 @@ import type { PongHistory200Response, PongHistory404Response, ProviderList200Response, + RemoveFriend200Response, + RemoveFriend404Response, Signin200Response, Signin400Response, Signin500Response, @@ -74,6 +79,10 @@ import type { TttHistory404Response, } from '../models/index'; import { + AddFriend200ResponseFromJSON, + AddFriend200ResponseToJSON, + AddFriend404ResponseFromJSON, + AddFriend404ResponseToJSON, AllowGuestMessage200ResponseFromJSON, AllowGuestMessage200ResponseToJSON, AllowGuestMessage403ResponseFromJSON, @@ -136,6 +145,8 @@ import { GuestLogin500ResponseToJSON, GuestLoginRequestFromJSON, GuestLoginRequestToJSON, + ListFriend200ResponseFromJSON, + ListFriend200ResponseToJSON, Login200ResponseFromJSON, Login200ResponseToJSON, Login202ResponseFromJSON, @@ -164,6 +175,10 @@ import { PongHistory404ResponseToJSON, ProviderList200ResponseFromJSON, ProviderList200ResponseToJSON, + RemoveFriend200ResponseFromJSON, + RemoveFriend200ResponseToJSON, + RemoveFriend404ResponseFromJSON, + RemoveFriend404ResponseToJSON, Signin200ResponseFromJSON, Signin200ResponseToJSON, Signin400ResponseFromJSON, @@ -190,6 +205,10 @@ import { TttHistory404ResponseToJSON, } from '../models/index'; +export interface AddFriendRequest { + user: string; +} + export interface ChangeDescOperationRequest { changeDescRequest: ChangeDescRequest; } @@ -226,6 +245,10 @@ export interface PongHistoryRequest { user: string; } +export interface RemoveFriendRequest { + user: string; +} + export interface SigninRequest { loginRequest: LoginRequest; } @@ -243,6 +266,60 @@ export interface TttHistoryRequest { */ export class OpenapiOtherApi extends runtime.BaseAPI { + /** + */ + async addFriendRaw(requestParameters: AddFriendRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['user'] == null) { + throw new runtime.RequiredError( + 'user', + 'Required parameter "user" was null or undefined when calling addFriend().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + + let urlPath = `/api/user/friend/add/{user}`; + urlPath = urlPath.replace(`{${"user"}}`, encodeURIComponent(String(requestParameters['user']))); + + const response = await this.request({ + path: urlPath, + method: 'PUT', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + // CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses + // This allows typed access to error responses (4xx, 5xx) and other status codes. + // The code routes responses based on the actual HTTP status code and returns + // appropriately typed ApiResponse wrappers for each status code. + if (response.status === 200) { + // Object response for status 200 + return new runtime.JSONApiResponse(response, (jsonValue) => AddFriend200ResponseFromJSON(jsonValue)); + } + if (response.status === 401) { + // Object response for status 401 + return new runtime.JSONApiResponse(response, (jsonValue) => ChangePassword401ResponseFromJSON(jsonValue)); + } + if (response.status === 404) { + // Object response for status 404 + return new runtime.JSONApiResponse(response, (jsonValue) => AddFriend404ResponseFromJSON(jsonValue)); + } + // CHANGED: Throw error if status code is not handled by any of the defined responses + // This ensures all code paths return a value and provides clear error messages for unexpected status codes + // Only throw if responses were defined but none matched the actual status code + throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401, 404`); + } + + /** + */ + async addFriend(requestParameters: AddFriendRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.addFriendRaw(requestParameters, initOverrides); + return await response.value(); + } + /** */ async allowGuestMessageRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { @@ -766,6 +843,48 @@ export class OpenapiOtherApi extends runtime.BaseAPI { return await response.value(); } + /** + */ + async listFriendRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + + let urlPath = `/api/user/friend/list`; + + const response = await this.request({ + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + // CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses + // This allows typed access to error responses (4xx, 5xx) and other status codes. + // The code routes responses based on the actual HTTP status code and returns + // appropriately typed ApiResponse wrappers for each status code. + if (response.status === 200) { + // Object response for status 200 + return new runtime.JSONApiResponse(response, (jsonValue) => ListFriend200ResponseFromJSON(jsonValue)); + } + if (response.status === 401) { + // Object response for status 401 + return new runtime.JSONApiResponse(response, (jsonValue) => StatusOtp401ResponseFromJSON(jsonValue)); + } + // CHANGED: Throw error if status code is not handled by any of the defined responses + // This ensures all code paths return a value and provides clear error messages for unexpected status codes + // Only throw if responses were defined but none matched the actual status code + throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401`); + } + + /** + */ + async listFriend(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.listFriendRaw(initOverrides); + return await response.value(); + } + /** */ async loginRaw(requestParameters: LoginOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { @@ -1016,6 +1135,60 @@ export class OpenapiOtherApi extends runtime.BaseAPI { return await response.value(); } + /** + */ + async removeFriendRaw(requestParameters: RemoveFriendRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['user'] == null) { + throw new runtime.RequiredError( + 'user', + 'Required parameter "user" was null or undefined when calling removeFriend().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + + let urlPath = `/api/user/friend/remove/{user}`; + urlPath = urlPath.replace(`{${"user"}}`, encodeURIComponent(String(requestParameters['user']))); + + const response = await this.request({ + path: urlPath, + method: 'PUT', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + // CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses + // This allows typed access to error responses (4xx, 5xx) and other status codes. + // The code routes responses based on the actual HTTP status code and returns + // appropriately typed ApiResponse wrappers for each status code. + if (response.status === 200) { + // Object response for status 200 + return new runtime.JSONApiResponse(response, (jsonValue) => RemoveFriend200ResponseFromJSON(jsonValue)); + } + if (response.status === 401) { + // Object response for status 401 + return new runtime.JSONApiResponse(response, (jsonValue) => ChangePassword401ResponseFromJSON(jsonValue)); + } + if (response.status === 404) { + // Object response for status 404 + return new runtime.JSONApiResponse(response, (jsonValue) => RemoveFriend404ResponseFromJSON(jsonValue)); + } + // CHANGED: Throw error if status code is not handled by any of the defined responses + // This ensures all code paths return a value and provides clear error messages for unexpected status codes + // Only throw if responses were defined but none matched the actual status code + throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401, 404`); + } + + /** + */ + async removeFriend(requestParameters: RemoveFriendRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.removeFriendRaw(requestParameters, initOverrides); + return await response.value(); + } + /** */ async signinRaw(requestParameters: SigninRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { diff --git a/frontend/src/api/generated/models/AddFriend200Response.ts b/frontend/src/api/generated/models/AddFriend200Response.ts new file mode 100644 index 0000000..0ce06f7 --- /dev/null +++ b/frontend/src/api/generated/models/AddFriend200Response.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface AddFriend200Response + */ +export interface AddFriend200Response { + /** + * + * @type {string} + * @memberof AddFriend200Response + */ + kind: AddFriend200ResponseKindEnum; + /** + * + * @type {string} + * @memberof AddFriend200Response + */ + msg: AddFriend200ResponseMsgEnum; +} + + +/** + * @export + */ +export const AddFriend200ResponseKindEnum = { + Success: 'success' +} as const; +export type AddFriend200ResponseKindEnum = typeof AddFriend200ResponseKindEnum[keyof typeof AddFriend200ResponseKindEnum]; + +/** + * @export + */ +export const AddFriend200ResponseMsgEnum = { + AddFriendSuccess: 'addFriend.success' +} as const; +export type AddFriend200ResponseMsgEnum = typeof AddFriend200ResponseMsgEnum[keyof typeof AddFriend200ResponseMsgEnum]; + + +/** + * Check if a given object implements the AddFriend200Response interface. + */ +export function instanceOfAddFriend200Response(value: object): value is AddFriend200Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function AddFriend200ResponseFromJSON(json: any): AddFriend200Response { + return AddFriend200ResponseFromJSONTyped(json, false); +} + +export function AddFriend200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): AddFriend200Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function AddFriend200ResponseToJSON(json: any): AddFriend200Response { + return AddFriend200ResponseToJSONTyped(json, false); +} + +export function AddFriend200ResponseToJSONTyped(value?: AddFriend200Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/AddFriend404Response.ts b/frontend/src/api/generated/models/AddFriend404Response.ts new file mode 100644 index 0000000..6431f79 --- /dev/null +++ b/frontend/src/api/generated/models/AddFriend404Response.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface AddFriend404Response + */ +export interface AddFriend404Response { + /** + * + * @type {string} + * @memberof AddFriend404Response + */ + kind: AddFriend404ResponseKindEnum; + /** + * + * @type {string} + * @memberof AddFriend404Response + */ + msg: AddFriend404ResponseMsgEnum; +} + + +/** + * @export + */ +export const AddFriend404ResponseKindEnum = { + Failure: 'failure' +} as const; +export type AddFriend404ResponseKindEnum = typeof AddFriend404ResponseKindEnum[keyof typeof AddFriend404ResponseKindEnum]; + +/** + * @export + */ +export const AddFriend404ResponseMsgEnum = { + AddFriendFailureUnknownUser: 'addFriend.failure.unknownUser' +} as const; +export type AddFriend404ResponseMsgEnum = typeof AddFriend404ResponseMsgEnum[keyof typeof AddFriend404ResponseMsgEnum]; + + +/** + * Check if a given object implements the AddFriend404Response interface. + */ +export function instanceOfAddFriend404Response(value: object): value is AddFriend404Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function AddFriend404ResponseFromJSON(json: any): AddFriend404Response { + return AddFriend404ResponseFromJSONTyped(json, false); +} + +export function AddFriend404ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): AddFriend404Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function AddFriend404ResponseToJSON(json: any): AddFriend404Response { + return AddFriend404ResponseToJSONTyped(json, false); +} + +export function AddFriend404ResponseToJSONTyped(value?: AddFriend404Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/ListFriend200Response.ts b/frontend/src/api/generated/models/ListFriend200Response.ts new file mode 100644 index 0000000..b354192 --- /dev/null +++ b/frontend/src/api/generated/models/ListFriend200Response.ts @@ -0,0 +1,110 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ListFriend200ResponsePayload } from './ListFriend200ResponsePayload'; +import { + ListFriend200ResponsePayloadFromJSON, + ListFriend200ResponsePayloadFromJSONTyped, + ListFriend200ResponsePayloadToJSON, + ListFriend200ResponsePayloadToJSONTyped, +} from './ListFriend200ResponsePayload'; + +/** + * + * @export + * @interface ListFriend200Response + */ +export interface ListFriend200Response { + /** + * + * @type {string} + * @memberof ListFriend200Response + */ + kind: ListFriend200ResponseKindEnum; + /** + * + * @type {string} + * @memberof ListFriend200Response + */ + msg: ListFriend200ResponseMsgEnum; + /** + * + * @type {ListFriend200ResponsePayload} + * @memberof ListFriend200Response + */ + payload: ListFriend200ResponsePayload; +} + + +/** + * @export + */ +export const ListFriend200ResponseKindEnum = { + Success: 'success' +} as const; +export type ListFriend200ResponseKindEnum = typeof ListFriend200ResponseKindEnum[keyof typeof ListFriend200ResponseKindEnum]; + +/** + * @export + */ +export const ListFriend200ResponseMsgEnum = { + ListFriendSuccess: 'listFriend.success' +} as const; +export type ListFriend200ResponseMsgEnum = typeof ListFriend200ResponseMsgEnum[keyof typeof ListFriend200ResponseMsgEnum]; + + +/** + * Check if a given object implements the ListFriend200Response interface. + */ +export function instanceOfListFriend200Response(value: object): value is ListFriend200Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + if (!('payload' in value) || value['payload'] === undefined) return false; + return true; +} + +export function ListFriend200ResponseFromJSON(json: any): ListFriend200Response { + return ListFriend200ResponseFromJSONTyped(json, false); +} + +export function ListFriend200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ListFriend200Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + 'payload': ListFriend200ResponsePayloadFromJSON(json['payload']), + }; +} + +export function ListFriend200ResponseToJSON(json: any): ListFriend200Response { + return ListFriend200ResponseToJSONTyped(json, false); +} + +export function ListFriend200ResponseToJSONTyped(value?: ListFriend200Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + 'payload': ListFriend200ResponsePayloadToJSON(value['payload']), + }; +} + diff --git a/frontend/src/api/generated/models/ListFriend200ResponsePayload.ts b/frontend/src/api/generated/models/ListFriend200ResponsePayload.ts new file mode 100644 index 0000000..51c2fa5 --- /dev/null +++ b/frontend/src/api/generated/models/ListFriend200ResponsePayload.ts @@ -0,0 +1,74 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ListFriend200ResponsePayloadFriendsInner } from './ListFriend200ResponsePayloadFriendsInner'; +import { + ListFriend200ResponsePayloadFriendsInnerFromJSON, + ListFriend200ResponsePayloadFriendsInnerFromJSONTyped, + ListFriend200ResponsePayloadFriendsInnerToJSON, + ListFriend200ResponsePayloadFriendsInnerToJSONTyped, +} from './ListFriend200ResponsePayloadFriendsInner'; + +/** + * + * @export + * @interface ListFriend200ResponsePayload + */ +export interface ListFriend200ResponsePayload { + /** + * + * @type {Array} + * @memberof ListFriend200ResponsePayload + */ + friends: Array; +} + +/** + * Check if a given object implements the ListFriend200ResponsePayload interface. + */ +export function instanceOfListFriend200ResponsePayload(value: object): value is ListFriend200ResponsePayload { + if (!('friends' in value) || value['friends'] === undefined) return false; + return true; +} + +export function ListFriend200ResponsePayloadFromJSON(json: any): ListFriend200ResponsePayload { + return ListFriend200ResponsePayloadFromJSONTyped(json, false); +} + +export function ListFriend200ResponsePayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): ListFriend200ResponsePayload { + if (json == null) { + return json; + } + return { + + 'friends': ((json['friends'] as Array).map(ListFriend200ResponsePayloadFriendsInnerFromJSON)), + }; +} + +export function ListFriend200ResponsePayloadToJSON(json: any): ListFriend200ResponsePayload { + return ListFriend200ResponsePayloadToJSONTyped(json, false); +} + +export function ListFriend200ResponsePayloadToJSONTyped(value?: ListFriend200ResponsePayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'friends': ((value['friends'] as Array).map(ListFriend200ResponsePayloadFriendsInnerToJSON)), + }; +} + diff --git a/frontend/src/api/generated/models/ListFriend200ResponsePayloadFriendsInner.ts b/frontend/src/api/generated/models/ListFriend200ResponsePayloadFriendsInner.ts new file mode 100644 index 0000000..de5ba72 --- /dev/null +++ b/frontend/src/api/generated/models/ListFriend200ResponsePayloadFriendsInner.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ListFriend200ResponsePayloadFriendsInner + */ +export interface ListFriend200ResponsePayloadFriendsInner { + /** + * + * @type {string} + * @memberof ListFriend200ResponsePayloadFriendsInner + */ + id: string; + /** + * + * @type {string} + * @memberof ListFriend200ResponsePayloadFriendsInner + */ + name: string; +} + +/** + * Check if a given object implements the ListFriend200ResponsePayloadFriendsInner interface. + */ +export function instanceOfListFriend200ResponsePayloadFriendsInner(value: object): value is ListFriend200ResponsePayloadFriendsInner { + if (!('id' in value) || value['id'] === undefined) return false; + if (!('name' in value) || value['name'] === undefined) return false; + return true; +} + +export function ListFriend200ResponsePayloadFriendsInnerFromJSON(json: any): ListFriend200ResponsePayloadFriendsInner { + return ListFriend200ResponsePayloadFriendsInnerFromJSONTyped(json, false); +} + +export function ListFriend200ResponsePayloadFriendsInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): ListFriend200ResponsePayloadFriendsInner { + if (json == null) { + return json; + } + return { + + 'id': json['id'], + 'name': json['name'], + }; +} + +export function ListFriend200ResponsePayloadFriendsInnerToJSON(json: any): ListFriend200ResponsePayloadFriendsInner { + return ListFriend200ResponsePayloadFriendsInnerToJSONTyped(json, false); +} + +export function ListFriend200ResponsePayloadFriendsInnerToJSONTyped(value?: ListFriend200ResponsePayloadFriendsInner | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'id': value['id'], + 'name': value['name'], + }; +} + diff --git a/frontend/src/api/generated/models/ListFriend404Response.ts b/frontend/src/api/generated/models/ListFriend404Response.ts new file mode 100644 index 0000000..251ea33 --- /dev/null +++ b/frontend/src/api/generated/models/ListFriend404Response.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ListFriend404Response + */ +export interface ListFriend404Response { + /** + * + * @type {string} + * @memberof ListFriend404Response + */ + kind: ListFriend404ResponseKindEnum; + /** + * + * @type {string} + * @memberof ListFriend404Response + */ + msg: ListFriend404ResponseMsgEnum; +} + + +/** + * @export + */ +export const ListFriend404ResponseKindEnum = { + Failure: 'failure' +} as const; +export type ListFriend404ResponseKindEnum = typeof ListFriend404ResponseKindEnum[keyof typeof ListFriend404ResponseKindEnum]; + +/** + * @export + */ +export const ListFriend404ResponseMsgEnum = { + RemoveFriendFailureUnknownUser: 'removeFriend.failure.unknownUser' +} as const; +export type ListFriend404ResponseMsgEnum = typeof ListFriend404ResponseMsgEnum[keyof typeof ListFriend404ResponseMsgEnum]; + + +/** + * Check if a given object implements the ListFriend404Response interface. + */ +export function instanceOfListFriend404Response(value: object): value is ListFriend404Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function ListFriend404ResponseFromJSON(json: any): ListFriend404Response { + return ListFriend404ResponseFromJSONTyped(json, false); +} + +export function ListFriend404ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ListFriend404Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function ListFriend404ResponseToJSON(json: any): ListFriend404Response { + return ListFriend404ResponseToJSONTyped(json, false); +} + +export function ListFriend404ResponseToJSONTyped(value?: ListFriend404Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/RemoveFriend200Response.ts b/frontend/src/api/generated/models/RemoveFriend200Response.ts new file mode 100644 index 0000000..aa3f7d7 --- /dev/null +++ b/frontend/src/api/generated/models/RemoveFriend200Response.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface RemoveFriend200Response + */ +export interface RemoveFriend200Response { + /** + * + * @type {string} + * @memberof RemoveFriend200Response + */ + kind: RemoveFriend200ResponseKindEnum; + /** + * + * @type {string} + * @memberof RemoveFriend200Response + */ + msg: RemoveFriend200ResponseMsgEnum; +} + + +/** + * @export + */ +export const RemoveFriend200ResponseKindEnum = { + Success: 'success' +} as const; +export type RemoveFriend200ResponseKindEnum = typeof RemoveFriend200ResponseKindEnum[keyof typeof RemoveFriend200ResponseKindEnum]; + +/** + * @export + */ +export const RemoveFriend200ResponseMsgEnum = { + RemoveFriendSuccess: 'removeFriend.success' +} as const; +export type RemoveFriend200ResponseMsgEnum = typeof RemoveFriend200ResponseMsgEnum[keyof typeof RemoveFriend200ResponseMsgEnum]; + + +/** + * Check if a given object implements the RemoveFriend200Response interface. + */ +export function instanceOfRemoveFriend200Response(value: object): value is RemoveFriend200Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function RemoveFriend200ResponseFromJSON(json: any): RemoveFriend200Response { + return RemoveFriend200ResponseFromJSONTyped(json, false); +} + +export function RemoveFriend200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): RemoveFriend200Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function RemoveFriend200ResponseToJSON(json: any): RemoveFriend200Response { + return RemoveFriend200ResponseToJSONTyped(json, false); +} + +export function RemoveFriend200ResponseToJSONTyped(value?: RemoveFriend200Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/RemoveFriend404Response.ts b/frontend/src/api/generated/models/RemoveFriend404Response.ts new file mode 100644 index 0000000..7987a33 --- /dev/null +++ b/frontend/src/api/generated/models/RemoveFriend404Response.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface RemoveFriend404Response + */ +export interface RemoveFriend404Response { + /** + * + * @type {string} + * @memberof RemoveFriend404Response + */ + kind: RemoveFriend404ResponseKindEnum; + /** + * + * @type {string} + * @memberof RemoveFriend404Response + */ + msg: RemoveFriend404ResponseMsgEnum; +} + + +/** + * @export + */ +export const RemoveFriend404ResponseKindEnum = { + Failure: 'failure' +} as const; +export type RemoveFriend404ResponseKindEnum = typeof RemoveFriend404ResponseKindEnum[keyof typeof RemoveFriend404ResponseKindEnum]; + +/** + * @export + */ +export const RemoveFriend404ResponseMsgEnum = { + RemoveFriendFailureUnknownUser: 'removeFriend.failure.unknownUser' +} as const; +export type RemoveFriend404ResponseMsgEnum = typeof RemoveFriend404ResponseMsgEnum[keyof typeof RemoveFriend404ResponseMsgEnum]; + + +/** + * Check if a given object implements the RemoveFriend404Response interface. + */ +export function instanceOfRemoveFriend404Response(value: object): value is RemoveFriend404Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function RemoveFriend404ResponseFromJSON(json: any): RemoveFriend404Response { + return RemoveFriend404ResponseFromJSONTyped(json, false); +} + +export function RemoveFriend404ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): RemoveFriend404Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function RemoveFriend404ResponseToJSON(json: any): RemoveFriend404Response { + return RemoveFriend404ResponseToJSONTyped(json, false); +} + +export function RemoveFriend404ResponseToJSONTyped(value?: RemoveFriend404Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInner.ts b/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInner.ts index 72f38ee..972dac5 100644 --- a/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInner.ts +++ b/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInner.ts @@ -13,13 +13,13 @@ */ import { mapValues } from '../runtime'; -import type { TttHistory200ResponsePayloadDataInnerPlayerX } from './TttHistory200ResponsePayloadDataInnerPlayerX'; +import type { ListFriend200ResponsePayloadFriendsInner } from './ListFriend200ResponsePayloadFriendsInner'; import { - TttHistory200ResponsePayloadDataInnerPlayerXFromJSON, - TttHistory200ResponsePayloadDataInnerPlayerXFromJSONTyped, - TttHistory200ResponsePayloadDataInnerPlayerXToJSON, - TttHistory200ResponsePayloadDataInnerPlayerXToJSONTyped, -} from './TttHistory200ResponsePayloadDataInnerPlayerX'; + ListFriend200ResponsePayloadFriendsInnerFromJSON, + ListFriend200ResponsePayloadFriendsInnerFromJSONTyped, + ListFriend200ResponsePayloadFriendsInnerToJSON, + ListFriend200ResponsePayloadFriendsInnerToJSONTyped, +} from './ListFriend200ResponsePayloadFriendsInner'; /** * @@ -35,16 +35,16 @@ export interface TttHistory200ResponsePayloadDataInner { gameId: string; /** * - * @type {TttHistory200ResponsePayloadDataInnerPlayerX} + * @type {ListFriend200ResponsePayloadFriendsInner} * @memberof TttHistory200ResponsePayloadDataInner */ - playerX: TttHistory200ResponsePayloadDataInnerPlayerX; + playerX: ListFriend200ResponsePayloadFriendsInner; /** * - * @type {TttHistory200ResponsePayloadDataInnerPlayerX} + * @type {ListFriend200ResponsePayloadFriendsInner} * @memberof TttHistory200ResponsePayloadDataInner */ - playerO: TttHistory200ResponsePayloadDataInnerPlayerX; + playerO: ListFriend200ResponsePayloadFriendsInner; /** * * @type {string} @@ -95,8 +95,8 @@ export function TttHistory200ResponsePayloadDataInnerFromJSONTyped(json: any, ig return { 'gameId': json['gameId'], - 'playerX': TttHistory200ResponsePayloadDataInnerPlayerXFromJSON(json['playerX']), - 'playerO': TttHistory200ResponsePayloadDataInnerPlayerXFromJSON(json['playerO']), + 'playerX': ListFriend200ResponsePayloadFriendsInnerFromJSON(json['playerX']), + 'playerO': ListFriend200ResponsePayloadFriendsInnerFromJSON(json['playerO']), 'date': json['date'], 'outcome': json['outcome'], }; @@ -114,8 +114,8 @@ export function TttHistory200ResponsePayloadDataInnerToJSONTyped(value?: TttHist return { 'gameId': value['gameId'], - 'playerX': TttHistory200ResponsePayloadDataInnerPlayerXToJSON(value['playerX']), - 'playerO': TttHistory200ResponsePayloadDataInnerPlayerXToJSON(value['playerO']), + 'playerX': ListFriend200ResponsePayloadFriendsInnerToJSON(value['playerX']), + 'playerO': ListFriend200ResponsePayloadFriendsInnerToJSON(value['playerO']), 'date': value['date'], 'outcome': value['outcome'], }; diff --git a/frontend/src/api/generated/models/index.ts b/frontend/src/api/generated/models/index.ts index a5a3877..b7e0ef1 100644 --- a/frontend/src/api/generated/models/index.ts +++ b/frontend/src/api/generated/models/index.ts @@ -1,5 +1,7 @@ /* tslint:disable */ /* eslint-disable */ +export * from './AddFriend200Response'; +export * from './AddFriend404Response'; export * from './AllowGuestMessage200Response'; export * from './AllowGuestMessage403Response'; export * from './ChangeDesc200Response'; @@ -37,6 +39,9 @@ export * from './GuestLogin200ResponsePayload'; export * from './GuestLogin400Response'; export * from './GuestLogin500Response'; export * from './GuestLoginRequest'; +export * from './ListFriend200Response'; +export * from './ListFriend200ResponsePayload'; +export * from './ListFriend200ResponsePayloadFriendsInner'; export * from './Login200Response'; export * from './Login202Response'; export * from './Login202ResponsePayload'; @@ -59,6 +64,8 @@ export * from './ProviderList200Response'; export * from './ProviderList200ResponsePayload'; export * from './ProviderList200ResponsePayloadListInner'; export * from './ProviderList200ResponsePayloadListInnerColors'; +export * from './RemoveFriend200Response'; +export * from './RemoveFriend404Response'; export * from './Signin200Response'; export * from './Signin200ResponsePayload'; export * from './Signin400Response'; @@ -81,5 +88,4 @@ export * from './TournamentList404Response'; export * from './TttHistory200Response'; export * from './TttHistory200ResponsePayload'; export * from './TttHistory200ResponsePayloadDataInner'; -export * from './TttHistory200ResponsePayloadDataInnerPlayerX'; export * from './TttHistory404Response'; diff --git a/src/@shared/src/database/index.ts b/src/@shared/src/database/index.ts index 2d30508..dcea4d9 100644 --- a/src/@shared/src/database/index.ts +++ b/src/@shared/src/database/index.ts @@ -4,17 +4,19 @@ import { FastifyInstance, FastifyPluginAsync } from 'fastify'; import { Database as DbImpl } from './mixin/_base'; import { IUserDb, UserImpl } from './mixin/user'; import { IBlockedDb, BlockedImpl } from './mixin/blocked'; +import { IFriendsDb, FriendsImpl } from './mixin/friends'; import { ITicTacToeDb, TicTacToeImpl } from './mixin/tictactoe'; import { IPongDb, PongImpl } from './mixin/pong'; import { ITournamentDb, TournamentImpl } from './mixin/tournament'; Object.assign(DbImpl.prototype, UserImpl); Object.assign(DbImpl.prototype, BlockedImpl); +Object.assign(DbImpl.prototype, FriendsImpl); Object.assign(DbImpl.prototype, TicTacToeImpl); Object.assign(DbImpl.prototype, PongImpl); Object.assign(DbImpl.prototype, TournamentImpl); -export interface Database extends DbImpl, IUserDb, IBlockedDb, ITicTacToeDb, IPongDb, ITournamentDb { } +export interface Database extends DbImpl, IUserDb, IBlockedDb, ITicTacToeDb, IPongDb, ITournamentDb, IFriendsDb { } // When using .decorate you have to specify added properties for Typescript declare module 'fastify' { diff --git a/src/@shared/src/database/init.sql b/src/@shared/src/database/init.sql index cb72332..41aae21 100644 --- a/src/@shared/src/database/init.sql +++ b/src/@shared/src/database/init.sql @@ -27,6 +27,15 @@ CREATE TABLE IF NOT EXISTS blocked ( CREATE UNIQUE INDEX IF NOT EXISTS idx_blocked_user_pair ON blocked (user, blocked); +CREATE TABLE IF NOT EXISTS friends ( + id INTEGER PRIMARY KEY NOT NULL, + user TEXT NOT NULL, + friend TEXT NOT NULL, + FOREIGN KEY (user) REFERENCES user (id) FOREIGN KEY (friend) REFERENCES user (id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_friends_user_pair ON friends (user, friend); + ---------------- -- TICTACTOE -- ---------------- diff --git a/src/@shared/src/database/mixin/friends.ts b/src/@shared/src/database/mixin/friends.ts new file mode 100644 index 0000000..916c99d --- /dev/null +++ b/src/@shared/src/database/mixin/friends.ts @@ -0,0 +1,72 @@ +import { isNullish } from '@shared/utils'; +import type { Database } from './_base'; +import { UserId } from './user'; + + +// describe every function in the object +export interface IFriendsDb extends Database { + getFriendsUserFor(id: UserId): FriendsData[], + addFriendsUserFor(id: UserId, friend: UserId): void, + removeFriendsUserFor(id: UserId, friend: UserId): void, + removeAllFriendUserFor(id: UserId): void, + getAllFriendsUsers(this: IFriendsDb): FriendsData[] | undefined, + +}; + +export const FriendsImpl: Omit = { + getFriendsUserFor(this: IFriendsDb, id: UserId): FriendsData[] { + const query = this.prepare('SELECT * FROM friends WHERE user = @id'); + const data = query.all({ id }) as Partial[]; + return data.map(friendsFromRow).filter(b => !isNullish(b)); + }, + + removeAllFriendUserFor(this: IFriendsDb, id: UserId): void { + this.prepare('DELETE FROM friends WHERE user = @id').run({ id }); + }, + addFriendsUserFor(this: IFriendsDb, id: UserId, friend: UserId): void { + this.prepare('INSERT OR IGNORE INTO friends (user, friend) VALUES (@id, @friend)').run({ id, friend }); + }, + removeFriendsUserFor(this: IFriendsDb, id: UserId, friend: UserId): void { + this.prepare('DELETE FROM friends WHERE user = @id AND friend = @friend').run({ id, friend }); + }, + + /** + * Get all friends user + * + * @param + * + * @returns The list of users if it exists, undefined otherwise + */ + getAllFriendsUsers(this: IFriendsDb): FriendsData[] { + const rows = this.prepare('SELECT * FROM friends').all() as Partial[]; + + return rows + .map(row => friendsFromRow(row)) + .filter((u): u is FriendsData => u !== undefined); + }, + +}; + +export type FriendsId = number & { readonly __brand: unique symbol }; + +export type FriendsData = { + readonly id: FriendsId; + readonly user: UserId; + readonly friend: UserId; +}; + +/** + * Get a friends from a row + * + * @param row The data from sqlite + * + * @returns The friends if it exists, undefined otherwise + */ +export function friendsFromRow(row?: Partial): FriendsData | undefined { + if (isNullish(row)) return undefined; + if (isNullish(row.id)) return undefined; + if (isNullish(row.user)) return undefined; + if (isNullish(row.friend)) return undefined; + + return row as FriendsData; +} diff --git a/src/icons/openapi.json b/src/icons/openapi.json new file mode 100644 index 0000000..38cd725 --- /dev/null +++ b/src/icons/openapi.json @@ -0,0 +1,21 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "9.6.1", + "title": "@fastify/swagger" + }, + "components": { + "schemas": {} + }, + "paths": {}, + "servers": [ + { + "url": "https://local.maix.me:8888", + "description": "direct from docker" + }, + { + "url": "https://local.maix.me:8000", + "description": "using fnginx" + } + ] +} diff --git a/src/icons/src/routes/set.ts b/src/icons/src/routes/set.ts index 5837a6c..b7aa917 100644 --- a/src/icons/src/routes/set.ts +++ b/src/icons/src/routes/set.ts @@ -6,18 +6,17 @@ import sharp from 'sharp'; import path from 'path'; import fs from 'node:fs/promises'; - export const IconSetRes = { '200': typeResponse('success', 'iconset.success'), - '400': typeResponse('success', ['iconset.failure.invalidFile', 'iconset.failure.noFile']), + '400': typeResponse('success', [ + 'iconset.failure.invalidFile', + 'iconset.failure.noFile', + ]), }; export type IconSetRes = MakeStaticResponse; -const validMimeTypes = new Set([ - 'image/jpeg', - 'image/png', -]); +const validMimeTypes = new Set(['image/jpeg', 'image/png']); async function resizeAndSaveImage( imageBuffer: Buffer, @@ -42,20 +41,42 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { await fastify.register(multipart); fastify.post( '/api/icons/set', - { schema: { response: IconSetRes, operationId: 'setIcons' }, config: { requireAuth: true } }, + { + schema: { + response: IconSetRes, + hide: true, + }, + config: { requireAuth: true }, + }, async function(req, res) { // req.authUser is always set, since this is gated const userid = req.authUser!.id; const file = await req.file(); if (!file) { - return res.makeResponse(400, 'failure', 'iconset.failure.noFile'); + return res.makeResponse( + 400, + 'failure', + 'iconset.failure.noFile', + ); } if (!validMimeTypes.has(file.mimetype)) { - return res.makeResponse(400, 'failure', 'iconset.failure.invalidFile'); + return res.makeResponse( + 400, + 'failure', + 'iconset.failure.invalidFile', + ); } const buf = await file.toBuffer(); - if (!validMimeTypes.has((await fileTypeFromBuffer(buf))?.mime ?? 'unknown')) { - return res.makeResponse(400, 'failure', 'iconset.failure.invalidFile'); + if ( + !validMimeTypes.has( + (await fileTypeFromBuffer(buf))?.mime ?? 'unknown', + ) + ) { + return res.makeResponse( + 400, + 'failure', + 'iconset.failure.invalidFile', + ); } try { resizeAndSaveImage(buf, userid); @@ -63,7 +84,11 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise => { } catch (e: unknown) { this.log.warn(e); - return res.makeResponse(400, 'failure', 'iconset.failure.invalidFile'); + return res.makeResponse( + 400, + 'failure', + 'iconset.failure.invalidFile', + ); } }, ); diff --git a/src/openapi.json b/src/openapi.json index d99dfc7..615cae1 100644 --- a/src/openapi.json +++ b/src/openapi.json @@ -1714,6 +1714,326 @@ ] } }, + "/api/user/friend/add/{user}": { + "put": { + "operationId": "addFriend", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "user", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "success" + ] + }, + "msg": { + "enum": [ + "addFriend.success" + ] + } + } + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + } + } + } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "addFriend.failure.unknownUser" + ] + } + } + } + } + } + } + }, + "tags": [ + "openapi_other" + ] + } + }, + "/api/user/friend/list": { + "get": { + "operationId": "listFriend", + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg", + "payload" + ], + "properties": { + "kind": { + "enum": [ + "success" + ] + }, + "msg": { + "enum": [ + "listFriend.success" + ] + }, + "payload": { + "type": "object", + "required": [ + "friends" + ], + "properties": { + "friends": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + }, + { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + } + ] + } + } + } + } + }, + "tags": [ + "openapi_other" + ] + } + }, + "/api/user/friend/remove/{user}": { + "put": { + "operationId": "removeFriend", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "user", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "success" + ] + }, + "msg": { + "enum": [ + "removeFriend.success" + ] + } + } + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + } + } + } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "removeFriend.failure.unknownUser" + ] + } + } + } + } + } + } + }, + "tags": [ + "openapi_other" + ] + } + }, "/api/user/info/{user}": { "get": { "operationId": "getUser", diff --git a/src/user/openapi.json b/src/user/openapi.json index 8f7df5f..1f34e15 100644 --- a/src/user/openapi.json +++ b/src/user/openapi.json @@ -476,6 +476,317 @@ } } }, + "/api/user/friend/add/{user}": { + "put": { + "operationId": "addFriend", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "user", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "success" + ] + }, + "msg": { + "enum": [ + "addFriend.success" + ] + } + } + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + } + } + } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "addFriend.failure.unknownUser" + ] + } + } + } + } + } + } + } + } + }, + "/api/user/friend/list": { + "get": { + "operationId": "listFriend", + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg", + "payload" + ], + "properties": { + "kind": { + "enum": [ + "success" + ] + }, + "msg": { + "enum": [ + "listFriend.success" + ] + }, + "payload": { + "type": "object", + "required": [ + "friends" + ], + "properties": { + "friends": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + }, + { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + } + ] + } + } + } + } + } + } + }, + "/api/user/friend/remove/{user}": { + "put": { + "operationId": "removeFriend", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "user", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "success" + ] + }, + "msg": { + "enum": [ + "removeFriend.success" + ] + } + } + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + } + } + } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "removeFriend.failure.unknownUser" + ] + } + } + } + } + } + } + } + } + }, "/api/user/info/{user}": { "get": { "operationId": "getUser", diff --git a/src/user/src/routes/friendAdd.ts b/src/user/src/routes/friendAdd.ts new file mode 100644 index 0000000..00bebd1 --- /dev/null +++ b/src/user/src/routes/friendAdd.ts @@ -0,0 +1,44 @@ +import { FastifyPluginAsync } from 'fastify'; +import { MakeStaticResponse, typeResponse } from '@shared/utils'; +import Type, { Static } from 'typebox'; + +export const AddFriendRes = { + '200': typeResponse('success', 'addFriend.success'), + '404': typeResponse('failure', 'addFriend.failure.unknownUser'), +}; + +export type AddFriendRes = MakeStaticResponse; + +const AddFriendParams = Type.Object({ + user: Type.String(), +}); +export type AddFriendParams = Static; + +const route: FastifyPluginAsync = async (fastify, _opts): Promise => { + void _opts; + fastify.put<{ Params: AddFriendParams }>( + '/api/user/friend/add/:user', + { + schema: { + params: AddFriendParams, + response: AddFriendRes, + operationId: 'addFriend', + }, + config: { requireAuth: true }, + }, + async function(req, res) { + const friend = this.db.getUser(req.params.user); + if (!friend) { + return res.makeResponse( + 404, + 'failure', + 'addFriend.failure.unknownUser', + ); + } + this.db.addFriendsUserFor(req.authUser!.id, friend.id); + return res.makeResponse(200, 'success', 'addFriend.success'); + }, + ); +}; + +export default route; diff --git a/src/user/src/routes/friendList.ts b/src/user/src/routes/friendList.ts new file mode 100644 index 0000000..4ef7f48 --- /dev/null +++ b/src/user/src/routes/friendList.ts @@ -0,0 +1,40 @@ +import { FastifyPluginAsync } from 'fastify'; +import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils'; +import Type, { Static } from 'typebox'; + +export const ListFriendRes = { + '200': typeResponse('success', 'listFriend.success', { + friends: Type.Array(Type.Object({ + id: Type.String(), + name: Type.String(), + })), + }), +}; + +export type ListFriendRes = MakeStaticResponse; + +const RemoveFriendParams = Type.Object({ + user: Type.String(), +}); +export type RemoveFriendParams = Static; + +const route: FastifyPluginAsync = async (fastify, _opts): Promise => { + void _opts; + fastify.get( + '/api/user/friend/list', + { + schema: { + response: ListFriendRes, + operationId: 'listFriend', + }, + config: { requireAuth: true }, + }, + async function(req, res) { + void req; + const friends: ListFriendRes['200']['payload']['friends'] = this.db.getFriendsUserFor(req.authUser!.id).map(v => this.db.getUser(v.friend)).filter(v => !isNullish(v)).map(v => ({ id: v.id, name: v.name })); + return res.makeResponse(200, 'success', 'listFriend.success', { friends }); + }, + ); +}; + +export default route; diff --git a/src/user/src/routes/friendRemove.ts b/src/user/src/routes/friendRemove.ts new file mode 100644 index 0000000..e4fc6f2 --- /dev/null +++ b/src/user/src/routes/friendRemove.ts @@ -0,0 +1,44 @@ +import { FastifyPluginAsync } from 'fastify'; +import { MakeStaticResponse, typeResponse } from '@shared/utils'; +import Type, { Static } from 'typebox'; + +export const RemoveFriendRes = { + '200': typeResponse('success', 'removeFriend.success'), + '404': typeResponse('failure', 'removeFriend.failure.unknownUser'), +}; + +export type RemoveFriendRes = MakeStaticResponse; + +const RemoveFriendParams = Type.Object({ + user: Type.String(), +}); +export type RemoveFriendParams = Static; + +const route: FastifyPluginAsync = async (fastify, _opts): Promise => { + void _opts; + fastify.put<{ Params: RemoveFriendParams }>( + '/api/user/friend/remove/:user', + { + schema: { + params: RemoveFriendParams, + response: RemoveFriendRes, + operationId: 'removeFriend', + }, + config: { requireAuth: true }, + }, + async function(req, res) { + const friend = this.db.getUser(req.params.user); + if (!friend) { + return res.makeResponse( + 404, + 'failure', + 'removeFriend.failure.unknownUser', + ); + } + this.db.removeFriendsUserFor(req.authUser!.id, friend.id); + return res.makeResponse(200, 'success', 'removeFriend.success'); + }, + ); +}; + +export default route; From c5eea6e29e4b1f7514de4f58089b1d0c1a0cd4bc Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Fri, 16 Jan 2026 16:47:41 +0100 Subject: [PATCH 07/10] update: friends list + button --- frontend/index.html | 1 + frontend/src/auth.ts | 5 +- frontend/src/chat/chat.ts | 109 +++++++++--------- .../chatHelperFunctions/actionBtnFriend.ts | 42 ++++--- frontend/src/pages/friendList/friendList.html | 9 ++ frontend/src/pages/friendList/friendList.ts | 41 +++++++ frontend/src/pages/index.ts | 2 +- frontend/src/utils.ts | 23 +++- 8 files changed, 156 insertions(+), 76 deletions(-) create mode 100644 frontend/src/pages/friendList/friendList.html create mode 100644 frontend/src/pages/friendList/friendList.ts diff --git a/frontend/index.html b/frontend/index.html index 59f7892..2182ae4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -37,6 +37,7 @@ ⭕ Tic-Tac-Toe ▮•▮ Ping Pong 🏆 Tournaments + 😎 Friends 🚪 Logout diff --git a/frontend/src/auth.ts b/frontend/src/auth.ts index 5c97eab..63491fc 100644 --- a/frontend/src/auth.ts +++ b/frontend/src/auth.ts @@ -1,7 +1,7 @@ import { showError } from "@app/toast"; import client from "@app/api"; import cookie from "js-cookie"; -import { ensureWindowState, isNullish } from "@app/utils"; +import { ensureWindowState, isNullish, updateFriendsList } from "@app/utils"; import { handleRoute, navigateTo } from "./routing"; cookie.remove("pkce"); @@ -105,10 +105,11 @@ if (!window.__state._headerProfile) { window.__state._reloadOnAuthChange ??= false; if (!window.__state._reloadOnAuthChange) { - document.addEventListener("ft:userChange", () => { + document.addEventListener("ft:userChange", async () => { // 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; + await updateFriendsList(); handleRoute(); }); window.__state._reloadOnAuthChange = true; diff --git a/frontend/src/chat/chat.ts b/frontend/src/chat/chat.ts index f3e2aa2..f52ce0b 100644 --- a/frontend/src/chat/chat.ts +++ b/frontend/src/chat/chat.ts @@ -39,6 +39,7 @@ let keysPressed: Record = {}; declare module "ft_state" { interface State { chatSock?: Socket; + friendList: { id: string; name: string }[]; } } @@ -82,18 +83,20 @@ const sendButton = document.getElementById("b-send") as HTMLButtonElement; const sendtextbox = document.getElementById( "t-chat-window", ) as HTMLButtonElement; -const systemWindow = document.getElementById("chat-system-box") as HTMLDivElement; +const systemWindow = document.getElementById( + "chat-system-box", +) as HTMLDivElement; function chatKeyToggle() { let anti_flicker_control = false; - const chat_hide_key = 'escape'; - const chat_display_key = 'f2'; - const home_display_key = 'f8'; + const chat_hide_key = "escape"; + const chat_display_key = "f2"; + const home_display_key = "f8"; document.addEventListener("keydown", (event) => { if (event.repeat && keysPressed[chat_hide_key] === true) { anti_flicker_control = true; - return ; - }; + return; + } keysPressed[event.key.toLowerCase()] = true; }); document.addEventListener("keyup", (event) => { @@ -102,34 +105,33 @@ function chatKeyToggle() { anti_flicker_control = false; } }); - setInterval( () => { - if(keysPressed[chat_hide_key] === true) { - overlay.classList.remove("opacity-60"); - chatBox.classList.add("hidden"); - chatMessageIn?.classList.add("hidden"); - chatMessageIn!.textContent = ''; - profilList?.classList.add("hidden"); - windowStateHidden(); + setInterval(() => { + if (keysPressed[chat_hide_key] === true) { + overlay.classList.remove("opacity-60"); + chatBox.classList.add("hidden"); + chatMessageIn?.classList.add("hidden"); + chatMessageIn!.textContent = ""; + profilList?.classList.add("hidden"); + windowStateHidden(); } if (keysPressed[chat_display_key] === true) { - anti_flicker_control = false; - chatBox.classList.remove("hidden"); - overlay.classList.add("opacity-60"); - chatMessageIn?.classList.add("hidden"); - chatMessageIn!.textContent = ''; - let socket = window.__state.chatSock; - if (!socket) return; - connected(socket); - sendtextbox.focus(); - windowStateVisable(); - + anti_flicker_control = false; + chatBox.classList.remove("hidden"); + overlay.classList.add("opacity-60"); + chatMessageIn?.classList.add("hidden"); + chatMessageIn!.textContent = ""; + let socket = window.__state.chatSock; + if (!socket) return; + connected(socket); + sendtextbox.focus(); + windowStateVisable(); } - if (keysPressed[home_display_key] === true) { - navigateTo('/app/'); + if (keysPressed[home_display_key] === true) { + navigateTo("/app/"); quitChat(); } - }, 1000/10); -}; + }, 1000 / 10); +} function initChatSocket() { let socket = getSocket(); @@ -152,9 +154,10 @@ function initChatSocket() { !profilList || !sendButton || !sendtextbox || - !systemWindow - ) return showError("fatal error"); - + !systemWindow + ) + return showError("fatal error"); + // Listen for the 'connect' event socket.on("connect", async () => { await waitSocketConnected(socket); @@ -202,10 +205,10 @@ function initChatSocket() { if (socket) { connected(socket); } - + if (chatWindow && data.message.destination === "") { chatMessageIn?.classList.remove("hidden"); - chatMessageIn!.textContent = '🔵'; + chatMessageIn!.textContent = "🔵"; const messageElement = document.createElement("div"); messageElement.textContent = `${data.message.user}: ${data.message.text}`; chatWindow.appendChild(messageElement); @@ -214,7 +217,7 @@ function initChatSocket() { if (chatWindow && data.message.destination === "privateMsg") { chatMessageIn?.classList.remove("hidden"); - chatMessageIn!.textContent = '🔴'; + chatMessageIn!.textContent = "🔴"; const messageElement = document.createElement("div-private"); messageElement.textContent = `🔒${data.message.user}: ${data.message.text}`; chatWindow.appendChild(messageElement); @@ -223,7 +226,7 @@ function initChatSocket() { if (chatWindow && data.message.destination === "inviteMsg") { chatMessageIn?.classList.remove("hidden"); - chatMessageIn!.textContent = '🟢'; + chatMessageIn!.textContent = "🟢"; const messageElement = document.createElement("div-private"); const chatWindow = document.getElementById( "t-chatbox", @@ -236,14 +239,13 @@ function initChatSocket() { if (systemWindow && data.message.destination === "system-info") { const messageElement = document.createElement("div"); messageElement.textContent = `${data.message.user}: ${data.message.text}`; - + // keep only last 10 while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) { systemWindow.removeChild(systemWindow.firstChild!); } systemWindow.appendChild(messageElement); systemWindow.lastElementChild?.scrollIntoView({ block: "end" }); - } }); @@ -278,9 +280,9 @@ function initChatSocket() { if (blockUserBtn) { let message = ""; if (data.userState === "block") { - (message = "un-block"); + message = "un-block"; } else { - (message = "block"); + message = "block"; } blockUserBtn.textContent = message; } @@ -294,17 +296,15 @@ function initChatSocket() { const htmlBaliseRegex = /]*>[\s\S]*?<\/a>/; const htmlBaliseMatch = message.match(htmlBaliseRegex); - if (htmlBaliseMatch) - addInviteMessage(message); - else - addMessage(message); + if (htmlBaliseMatch) addInviteMessage(message); + else addMessage(message); }); //receives broadcast of the next GAME socket.on("nextGame", (message: string) => { openMessagePopup(message); }); - + //receives broadcast of the next GAME socket.on("tourStatus", (message: string) => { openMessagePopup(message); @@ -391,20 +391,20 @@ sendButton?.addEventListener("click", () => { } break; - case "@pong": + case "@pong": if (msgCommand[1] === "") { navigateTo("/app/pong/games"); quitChat(); - } + } break; - case "@ttt": + case "@ttt": if (msgCommand[1] === "") { navigateTo("/app/ttt/games"); quitChat(); - } + } break; - + case "@guest": if (!userId) { return; @@ -505,7 +505,6 @@ clearText?.addEventListener("click", () => { bquit?.addEventListener("click", () => { quitChat(); - }); myGames?.addEventListener("click", () => { @@ -527,7 +526,7 @@ sendtextbox.addEventListener("keydown", (event) => { } }); -chatButton!.addEventListener("click",() => { +chatButton!.addEventListener("click", () => { if (chatBox.classList.contains("hidden")) { chatBox.classList.toggle("hidden"); overlay.classList.add("opacity-60"); @@ -536,14 +535,14 @@ chatButton!.addEventListener("click",() => { if (!socket) return; connected(socket); chatMessageIn?.classList.add("hidden"); - chatMessageIn!.textContent = ''; - sendtextbox.focus(); + chatMessageIn!.textContent = ""; + sendtextbox.focus(); } else { chatBox.classList.toggle("hidden"); overlay.classList.remove("opacity-60"); windowStateHidden(); chatMessageIn?.classList.add("hidden"); - chatMessageIn!.textContent = ''; + chatMessageIn!.textContent = ""; } }); diff --git a/frontend/src/chat/chatHelperFunctions/actionBtnFriend.ts b/frontend/src/chat/chatHelperFunctions/actionBtnFriend.ts index c804154..8ca45ec 100644 --- a/frontend/src/chat/chatHelperFunctions/actionBtnFriend.ts +++ b/frontend/src/chat/chatHelperFunctions/actionBtnFriend.ts @@ -1,5 +1,8 @@ +import client from "@app/api"; import type { ClientProfil } from "../types_front"; import { Socket } from "socket.io-client"; +import { showError, showSuccess } from "@app/toast"; +import { getFriendList, updateFriendsList } from "@app/utils"; /** * function listens for a click on the TTT game History button @@ -8,20 +11,25 @@ import { Socket } from "socket.io-client"; **/ export function actionBtnFriend(profile: ClientProfil, senderSocket: Socket) { - setTimeout(() => { - const friend = document.querySelector("#btn-friend"); - friend?.addEventListener("click", () => { - - if (friend.textContent = "friend") { - friend.textContent = "not-friend" - console.log('friend'); - } - else { - friend.textContent = "not-friend" - console.log('Not a friend'); - - } - - }); - }, 0) -}; \ No newline at end of file + setTimeout(() => { + const friend = document.querySelector("#btn-friend"); + friend?.addEventListener("click", async () => { + let friendList = getFriendList(); + if (!friendList.some(v => v.id === profile.userID!)) { + let req = await client.addFriend({ user: profile.userID! }); + if (req.kind === 'success') + showSuccess('Successfully added a new Friend') + else + showError('Failed to add a new Friend'); + } + else { + let req = await client.removeFriend({ user: profile.userID! }); + if (req.kind === 'success') + showSuccess('Successfully removed a Friend') + else + showError('Failed to remove a Friend'); + } + await updateFriendsList(); + }); + }, 0) +}; diff --git a/frontend/src/pages/friendList/friendList.html b/frontend/src/pages/friendList/friendList.html new file mode 100644 index 0000000..68f64db --- /dev/null +++ b/frontend/src/pages/friendList/friendList.html @@ -0,0 +1,9 @@ +
+
+

+ FriendList +

+
+
+
diff --git a/frontend/src/pages/friendList/friendList.ts b/frontend/src/pages/friendList/friendList.ts new file mode 100644 index 0000000..83fb2df --- /dev/null +++ b/frontend/src/pages/friendList/friendList.ts @@ -0,0 +1,41 @@ +import { addRoute, navigateTo, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; +import page from './friendList.html?raw'; +import { getFriendList, isNullish, updateFriendsList } from "@app/utils"; +import client from "@app/api"; +import { updateUser } from "@app/auth"; +import { showError } from "@app/toast"; + + +async function friends(_url: string, args: RouteHandlerParams): Promise { + setTitle("Tic Tac Toe Games"); + let user = await updateUser(); + if (isNullish(user)) { + return { html: ' You aren\'t logged in ', postInsert: () => { showError("You must be logged in !"); navigateTo("/") } }; + } + await updateFriendsList(); + let friendList = getFriendList(); + friendList.sort(); + + let friendsElem = friendList.map(g => { + let e = document.createElement('div'); + e.className = 'grid grid-cols-[1fr_auto_1fr] items-center bg-zinc-800 rounded-lg px-4 py-3'; + + e.innerHTML = ` +
${g.name}
+ TTT Games + Pong Games + `; + return e; + }).filter(v => !isNullish(v)); + + return { + html: page, postInsert: async (app) => { + if (!app) return; + const friendsBox = app.querySelector("#friendList"); + if (!friendsBox) return; + friendsElem.forEach(c => friendsBox.appendChild(c)); + } + }; +} + +addRoute('/friends', friends); diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index cd6c41d..aef2743 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -1,6 +1,5 @@ import { setTitle, handleRoute } from '@app/routing'; import './root/root.ts' -import '../chat/chat.ts' import './pong/pong.ts' import './login/login.ts' import './signin/signin.ts' @@ -10,6 +9,7 @@ import './logout/logout.ts' import './pongHistory/pongHistory.ts' import './tttHistory/tttHistory.ts' import './tourHistory/tourHistory.ts' +import './friendList/friendList.ts' // ---- Initial load ---- setTitle(""); diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 80a3888..6229799 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -1,9 +1,11 @@ +import client from "./api"; + export function escapeHTML(str: string): string { const p = document.createElement("p"); p.appendChild(document.createTextNode(str)); return p.innerHTML; } -export function isNullish(v: T | undefined | null): v is (null | undefined) { +export function isNullish(v: T | undefined | null): v is null | undefined { return v === null || v === undefined; } @@ -11,3 +13,22 @@ export function isNullish(v: T | undefined | null): v is (null | undefined) { export function ensureWindowState() { window.__state = window.__state ?? {}; } + +export async function updateFriendsList() { + window.__state = window.__state ?? {}; + window.__state.friendList ??= []; + + try { + let req = await client.listFriend(); + if (req.kind === "success") { + window.__state.friendList = req.payload.friends; + } + } catch (e: unknown) { } +} + +export function getFriendList() { + ensureWindowState(); + window.__state.friendList ??= []; + + return window.__state.friendList; +} From ef568c9899edc62c5b15121394f9855f40a6e274 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Fri, 16 Jan 2026 17:00:21 +0100 Subject: [PATCH 08/10] yes --- src/pong/src/state.ts | 8 +++++++- src/pong/src/tour.ts | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pong/src/state.ts b/src/pong/src/state.ts index f73353a..e3f0aff 100644 --- a/src/pong/src/state.ts +++ b/src/pong/src/state.ts @@ -279,6 +279,12 @@ class StateI { const gameId = newUUID() as unknown as GameId; this.games.set(gameId, g); + setTimeout(() => { + if (!g.ready_checks[0] && !g.ready_checks[1]) { + this.fastify.log.info(`paused game ${gameId} has been canceled`); + this.cleanupGame(gameId, g); + } + }, 1000 * 31); this.fastify.log.info('new paused game \'' + gameId + '\''); return gameId; } @@ -573,7 +579,7 @@ class StateI { game.local, ); this.fastify.log.info('SetGameOutcome !'); - if (!game.local) { + if (!game.local && game.ready_checks[0] && game.ready_checks[1]) { const payload = { nextGame: chat_text }; try { const resp = await fetch('http://app-chat/broadcastNextGame', { diff --git a/src/pong/src/tour.ts b/src/pong/src/tour.ts index b7df36f..cf82fab 100644 --- a/src/pong/src/tour.ts +++ b/src/pong/src/tour.ts @@ -70,6 +70,7 @@ export class Tournament { const [u1, u2] = matchup; const gameId = newUUID() as PongGameId; const game = State.initGame(null, gameId, u1, u2); + State.broadcastTourStatus(`A Tournament game between ${this.users.get(u1)?.name ?? 'the left player'} and ${this.users.get(u2)?.name ?? 'the right player'} will start ASAP`); if (game) { game.onEnd = () => this.gameEnd(); } From 48c33b356830dcdaab676249052eab5898cdf094 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Fri, 16 Jan 2026 17:02:42 +0100 Subject: [PATCH 09/10] 60s to rm game --- src/pong/src/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pong/src/state.ts b/src/pong/src/state.ts index e3f0aff..fd77adc 100644 --- a/src/pong/src/state.ts +++ b/src/pong/src/state.ts @@ -284,7 +284,7 @@ class StateI { this.fastify.log.info(`paused game ${gameId} has been canceled`); this.cleanupGame(gameId, g); } - }, 1000 * 31); + }, 1000 * 60); this.fastify.log.info('new paused game \'' + gameId + '\''); return gameId; } From 4cb0104124c447253d9035a01e443d93098626ab Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Sat, 17 Jan 2026 10:42:51 +0100 Subject: [PATCH 10/10] yes --- frontend/src/pages/pong/pong.ts | 12 +++++++++--- src/pong/src/state.ts | 30 +++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/pong/pong.ts b/frontend/src/pages/pong/pong.ts index 3dd40ed..d8c09e3 100644 --- a/frontend/src/pages/pong/pong.ts +++ b/frontend/src/pages/pong/pong.ts @@ -107,7 +107,9 @@ function tourinfoButtons(tourInfo : HTMLButtonElement, tourScoreScreen : HTMLDiv }); } -function gameJoinButtons(socket : CSocket, inTournament : boolean, currentGame : currentGameInfo | null, +let inTournament: boolean = false; + +function gameJoinButtons(socket : CSocket, currentGame : currentGameInfo | null, tournament : HTMLButtonElement, queue : HTMLButtonElement, localGame : HTMLButtonElement, ready : HTMLButtonElement) { tournament.addEventListener("click", () => { @@ -150,6 +152,10 @@ function gameJoinButtons(socket : CSocket, inTournament : boolean, currentGame : } }); localGame.addEventListener("click", () => { + if (inTournament) { + showError("You can't queue up currently !"); + return; + } if ( queue.innerText !== QueueState.Iddle || currentGame !== null || @@ -274,7 +280,7 @@ function pongClient( setTitle("Pong Game"); const urlParams = new URLSearchParams(window.location.search); let game_req_join = urlParams.get("game"); - let inTournament = false; + inTournament = false; return { html: authHtml, @@ -548,7 +554,7 @@ function pongClient( setInterval(() => {keys_listen_setup(currentGame, socket, keys, playHow, playHow_b, tourScoreScreen, queue)}, 1000 / 60); - gameJoinButtons(socket, inTournament, currentGame, tournament, queue, localGame, ready); + gameJoinButtons(socket, currentGame, tournament, queue, localGame, ready); playhowButtons(playHow_b, playHow); tourinfoButtons(tourInfo, tourScoreScreen); diff --git a/src/pong/src/state.ts b/src/pong/src/state.ts index fd77adc..e0bd729 100644 --- a/src/pong/src/state.ts +++ b/src/pong/src/state.ts @@ -160,8 +160,20 @@ class StateI { }); return; } - - this.dequeueUser(user.socket); + if (user.currentGame !== null) { + sock.emit('tournamentRegister', { + kind: 'failure', + msg: 'You are in game', + }); + return; + } + if (this.queue.has(user.id)) { + sock.emit('tournamentRegister', { + kind: 'failure', + msg: 'You are in queue', + }); + return; + } this.tournament.addUser(user.id, name ?? udb.name); sock.emit('tournamentRegister', { kind: 'success', @@ -281,7 +293,9 @@ class StateI { this.games.set(gameId, g); setTimeout(() => { if (!g.ready_checks[0] && !g.ready_checks[1]) { - this.fastify.log.info(`paused game ${gameId} has been canceled`); + this.fastify.log.info( + `paused game ${gameId} has been canceled`, + ); this.cleanupGame(gameId, g); } }, 1000 * 60); @@ -444,13 +458,19 @@ class StateI { ) { this.fastify.log.warn( 'user trying to connect to a game he\'s not part of: gameId:' + - g_id + ' userId:' + sock.authUser.id); + g_id + + ' userId:' + + sock.authUser.id, + ); return JoinRes.no; } if (game.userOnPage[0] === true && game.userOnPage[1] === true) { this.fastify.log.warn( 'user trying to connect to a game he\'s already joined: gameId:' + - g_id + ' userId:' + sock.authUser.id); + g_id + + ' userId:' + + sock.authUser.id, + ); return JoinRes.no; } game.userOnPage[game.userLeft === sock.authUser.id ? 0 : 1] = true;