Compare commits

..

10 commits

Author SHA1 Message Date
Maieul BOYER
4cb0104124
yes
All checks were successful
Build / build (push) Successful in 18s
Linter / Linter (push) Successful in 17s
2026-01-17 10:42:51 +01:00
Maieul BOYER
48c33b3568 60s to rm game 2026-01-16 17:04:03 +01:00
Maieul BOYER
ef568c9899 yes 2026-01-16 17:04:03 +01:00
Maieul BOYER
c5eea6e29e update: friends list + button 2026-01-16 16:50:53 +01:00
Maieul BOYER
590604b385 friends-backend 2026-01-16 16:22:32 +01:00
NigeParis
df79bc5a80 Image to profil 2026-01-16 16:08:23 +01:00
Maieul BOYER
5884407f35 feat(frontend): you can now update your icon 2026-01-16 15:52:44 +01:00
Maieul BOYER
3cccc18e9a icons started to work yeah 2026-01-16 15:52:44 +01:00
NigeParis
95784c9719 Added function to buttun friend actionBtnFriend 2026-01-16 15:49:11 +01:00
NigeParis
c547c2301f button friend in profile 2026-01-16 15:49:11 +01:00
56 changed files with 2817 additions and 106 deletions

4
.gitignore vendored
View file

@ -22,3 +22,7 @@ pnpm-lock.yaml
package-lock.json
.idea
.dev
openapi-template
nginx-dev
src/redocly.yaml
flake/

View file

@ -34,7 +34,8 @@ ifeq "$(REDUCED_SET)" "y"
tic-tac-toe \
nginx \
user \
pong
pong \
icons
endif
all: build

View file

@ -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:

View file

@ -37,6 +37,7 @@
<a href="/ttt" class="hover:bg-gray-700 rounded-md px-3 py-2">⭕ Tic-Tac-Toe</a>
<a href="/pong" class="hover:bg-gray-700 rounded-md px-3 py-2">▮•▮ Ping Pong</a>
<a href="/tour" class="hover:bg-gray-700 rounded-md px-3 py-2">🏆 Tournaments</a>
<a href="/friends" class="hover:bg-gray-700 rounded-md px-3 py-2">😎 Friends</a>
<a href="/logout" class="hover:bg-gray-700 rounded-md px-3 py-2">🚪 Logout</a>
</nav>
</aside>

View file

@ -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

View file

@ -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<runtime.ApiResponse<AddFriend200Response | ChangePassword401Response | AddFriend404Response>> {
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<AddFriend200Response | ChangePassword401Response | AddFriend404Response> {
const response = await this.addFriendRaw(requestParameters, initOverrides);
return await response.value();
}
/**
*/
async allowGuestMessageRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AllowGuestMessage200Response | StatusOtp401Response | AllowGuestMessage403Response>> {
@ -766,6 +843,48 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
return await response.value();
}
/**
*/
async listFriendRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ListFriend200Response | StatusOtp401Response>> {
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<ListFriend200Response | StatusOtp401Response> {
const response = await this.listFriendRaw(initOverrides);
return await response.value();
}
/**
*/
async loginRaw(requestParameters: LoginOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Login200Response | Login202Response | Login400Response>> {
@ -1016,6 +1135,60 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
return await response.value();
}
/**
*/
async removeFriendRaw(requestParameters: RemoveFriendRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<RemoveFriend200Response | ChangePassword401Response | RemoveFriend404Response>> {
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<RemoveFriend200Response | ChangePassword401Response | RemoveFriend404Response> {
const response = await this.removeFriendRaw(requestParameters, initOverrides);
return await response.value();
}
/**
*/
async signinRaw(requestParameters: SigninRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Signin200Response | Signin400Response | Signin500Response>> {

View file

@ -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'],
};
}

View file

@ -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'],
};
}

View file

@ -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']),
};
}

View file

@ -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<ListFriend200ResponsePayloadFriendsInner>}
* @memberof ListFriend200ResponsePayload
*/
friends: Array<ListFriend200ResponsePayloadFriendsInner>;
}
/**
* 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<any>).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<any>).map(ListFriend200ResponsePayloadFriendsInnerToJSON)),
};
}

View file

@ -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'],
};
}

View file

@ -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'],
};
}

View file

@ -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'],
};
}

View file

@ -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'],
};
}

View file

@ -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'],
};

View file

@ -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';

View file

@ -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;

View file

@ -291,6 +291,13 @@ div-private {
right-12
}
.popup-b-friend {
@apply
absolute
bottom-32
left-12
}
.popup-b-game {
@apply
absolute

View file

@ -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;
@ -38,6 +39,7 @@ let keysPressed: Record<string, boolean> = {};
declare module "ft_state" {
interface State {
chatSock?: Socket;
friendList: { id: string; name: string }[];
}
}
@ -81,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) => {
@ -101,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();
@ -151,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);
@ -201,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);
@ -213,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);
@ -222,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",
@ -235,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" });
}
});
@ -256,6 +259,7 @@ function initChatSocket() {
actionBtnPopUpBlock(profil, socket);
actionBtnPongGames(profil, socket);
actionBtnTTTGames(profil, socket);
actionBtnFriend(profil, socket);
});
socket.on("blockUser", (blocked: ClientProfil) => {
@ -276,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;
}
@ -292,17 +296,15 @@ function initChatSocket() {
const htmlBaliseRegex = /<a\b[^>]*>[\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);
@ -389,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;
@ -503,7 +505,6 @@ clearText?.addEventListener("click", () => {
bquit?.addEventListener("click", () => {
quitChat();
});
myGames?.addEventListener("click", () => {
@ -525,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");
@ -534,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 = "";
}
});

View file

@ -0,0 +1,35 @@
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
* @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", 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)
};

View file

@ -6,17 +6,18 @@ export async function openProfilePopup(profil: ClientProfil) {
modalname.innerHTML =
`
<div class="profile-info">
<div-profil-name id="profilName" class="text-xl font-bold text-blue-500"> Profile of ${profil.user} </div>
<div-profil-name id="profilName" class="text-xl font-bold text-blue-500"> Profile of ${profil.user} </div>
<div-login-name id="loginName"> Login status: <span class="recessed">${profil.loginName ?? 'Guest'}</span> </div>
</br>
<div-login-name id="loginName"> Login ID: <span class="recessed">${profil.userID ?? ''}</span> </div>
<div-login-name id="loginName"> Login ID: <span class="recessed">${profil.userID ?? ''}</span> </div>
<img src="/icons/${profil.userID}" class="w-30 h-30 absoluet absolute top-26 right-10" > </img>
</br>
<button id="popup-b-invite" class="btn-style popup-b-invite">U Game ?</button>
<button id="popup-b-block" class="btn-style popup-b-block">Block User</button>
<div id="profile-about" class="text-2xl">About: <span class="recessed">${profil.text}</span> </div>
<button id="popup-b-hGame" class="btn-style-games popup-b-game">View Pong Games</button>
<button id="popup-b-hTGame" class="btn-style-games popup-b-TTTgame">View TTT Games</button>
<button id="btn-friend" class="btn-style popup-b-friend">friend</button>
</div>
`;
const profilList = document.getElementById("profile-modal") ?? null;

View file

@ -0,0 +1,9 @@
<div class="fixed inset-0 flex items-center justify-center bg-[#43536b]">
<div
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-200 w-212.5 p-6 rounded-xl shadow-2xl text-center z-50">
<h2 class="text-2xl font-bold text-center mb-4 text-gray-700">
FriendList
</h2>
<div id="friendList" class="max-w-3xl mx-auto space-y-2 max-h-[50dvh] overflow-scroll"></div>
</div>
</div>

View file

@ -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<RouteHandlerReturn> {
setTitle("Tic Tac Toe Games");
let user = await updateUser();
if (isNullish(user)) {
return { html: '<span> You aren\'t logged in </span>', 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 = `
<div class="text-center font-semibold text-white">${g.name}</div>
<a href="/ttt/games/${g.id}" class="text-center">TTT Games</a>
<a href="/pong/games/${g.id}" class="text-center">Pong Games</a>
`;
return e;
}).filter(v => !isNullish(v));
return {
html: page, postInsert: async (app) => {
if (!app) return;
const friendsBox = app.querySelector<HTMLDivElement>("#friendList");
if (!friendsBox) return;
friendsElem.forEach(c => friendsBox.appendChild(c));
}
};
}
addRoute('/friends', friends);

View file

@ -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("");

View file

@ -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);

View file

@ -31,6 +31,19 @@
</div>
</div>
</div>
<div id="iconBox" class="mb-1 grid grid-cols-[auto_1fr] gap-2 hidden">
<div id="viewIconBox" class="h-[128px] w-[128px] p-2 border-gray-700 border-2 rounded-lg">
<img/>
</div>
<div id="updateIconBox">
<form id="updateIconForm" class="flex flex-col justify-evenly h-full">
<input type="file" name="upload" accept="image/png, image/jpeg" class="text-black border-black border-2 rounded-lg px-4 w-[230px]"></input>
<span class="text-black font-base">Select Image</span>
<button type="submit" class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700"> Update </button>
</form>
</div>
</div>
<!-- Display Name -->
<div id="displayNameWrapper" class="py-2">
<label class="block font-medium mb-1 text-gray-700">Display Name</label>
@ -52,8 +65,7 @@
<label class="block font-medium mb-1 text-gray-700">Description</label>
<input id="descBox" type="text" placeholder="Description..." name="Description"
class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button id="descButton"
class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">Update</button>
<button id="descButton" class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">Update</button>
</div>
<!-- TOTP -->
<div class="border rounded p-4" id="totpWrapper" hidden>

View file

@ -40,6 +40,45 @@ function removeBgColor(...elem: HTMLElement[]) {
}
}
async function setup_profile_image(container: HTMLDivElement, url: string) {
let imgNode = container.querySelector<HTMLImageElement>("img");
let formNode = container.querySelector<HTMLFormElement>("form");
if (!imgNode || !formNode) return;
imgNode.src = url;
container.classList.remove("hidden");
formNode.addEventListener("submit", async (e) => {
e.preventDefault();
let form = e.target;
if (!form) return;
let data = new FormData(form as HTMLFormElement);
let req = await fetch("/api/icons/set", {
body: data,
method: "POST",
});
if (req.status === 200 || req.status === 400) {
let json = await req.json();
if (!("kind" in json) || !("msg" in json))
return showError("Unknown Error");
if (typeof json.kind !== "string" || typeof json.msg !== "string")
return showError("Unknown Error");
const pjson: { kind: string; msg: string } = json;
if (pjson.kind === "success") {
showSuccess("Updated image !");
return handleRoute();
} else {
console.log(`Failed to upload image: ${pjson.msg}`);
showError("Failed to change image");
}
} if (req.status === 413)
{
showError("Image too big");
}
else {
showError("Unknown Error");
}
});
}
async function route(url: string, _args: { [k: string]: string }) {
setTitle("Edit Profile");
return {
@ -99,8 +138,7 @@ async function route(url: string, _args: { [k: string]: string }) {
let descWrapper =
app.querySelector<HTMLDivElement>("#descWrapper")!;
let descBox =
app.querySelector<HTMLInputElement>("#descBox")!;
let descBox = app.querySelector<HTMLInputElement>("#descBox")!;
let descButton =
app.querySelector<HTMLButtonElement>("#descButton")!;
@ -126,6 +164,8 @@ async function route(url: string, _args: { [k: string]: string }) {
let totpWrapper =
app.querySelector<HTMLDivElement>("#totpWrapper")!;
let imgBox = app.querySelector<HTMLDivElement>("#iconBox")!;
descBox.value = user.desc;
if (user.guest) {
@ -138,10 +178,7 @@ async function route(url: string, _args: { [k: string]: string }) {
descButton,
);
descButton.classList.add(
"bg-gray-700",
"hover:bg-gray-700",
);
descButton.classList.add("bg-gray-700", "hover:bg-gray-700");
descButton.disabled = true;
descBox.disabled = true;
@ -174,6 +211,7 @@ async function route(url: string, _args: { [k: string]: string }) {
passwordWrapper.hidden = false;
accountTypeBox.innerText = "Normal";
setup_profile_image(imgBox, `/icons/${user.id}`);
} else if (
!isNullish(user.selfInfo?.providerId) &&
!isNullish(user.selfInfo?.providerUser)
@ -195,6 +233,7 @@ async function route(url: string, _args: { [k: string]: string }) {
totpWrapper.hidden = true;
accountTypeBox.innerText = "Provider";
setup_profile_image(imgBox, `/icons/${user.id}`);
}
// ---- Update UI ----
@ -269,12 +308,13 @@ async function route(url: string, _args: { [k: string]: string }) {
}
};
descButton.onclick = async () => {
let req = await client.changeDesc({ changeDescRequest: { desc: descBox.value } });
let req = await client.changeDesc({
changeDescRequest: { desc: descBox.value },
});
if (req.kind === "success") {
showSuccess("Successfully changed description");
handleRoute();
}
else {
} else {
showError(`Failed to update`);
}
};

View file

@ -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<T>(v: T | undefined | null): v is (null | undefined) {
export function isNullish<T>(v: T | undefined | null): v is null | undefined {
return v === null || v === undefined;
}
@ -11,3 +13,22 @@ export function isNullish<T>(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;
}

View file

@ -0,0 +1,10 @@
#forward the post request to the microservice
location /api/icons/ {
proxy_pass http://app-icons;
}
location /icons/ {
root /volumes/;
default_type image/png;
add_header Cache-Control "max-age=30";
}

View file

@ -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;

View file

@ -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' {

View file

@ -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 --
----------------

View file

@ -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<IFriendsDb, keyof Database> = {
getFriendsUserFor(this: IFriendsDb, id: UserId): FriendsData[] {
const query = this.prepare('SELECT * FROM friends WHERE user = @id');
const data = query.all({ id }) as Partial<FriendsData>[];
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<FriendsData>[];
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>): 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;
}

BIN
src/auth/config/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -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';
export const GuestLoginRes = {
'500': typeResponse('failed', [
@ -91,6 +92,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
'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()),
});

View file

@ -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<void> => {
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...');

View file

@ -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<void> => {
}
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);

2
src/icons/.dockerignore Normal file
View file

@ -0,0 +1,2 @@
/dist
/node_modules

21
src/icons/openapi.json Normal file
View file

@ -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"
}
]
}

35
src/icons/package.json Normal file
View file

@ -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"
}
}

33
src/icons/src/app.ts Normal file
View file

@ -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> => {
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 };

21
src/icons/src/openapi.ts Normal file
View file

@ -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();

View file

@ -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/).

View file

@ -0,0 +1,97 @@
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<typeof IconSetRes>;
const validMimeTypes = new Set(['image/jpeg', 'image/png']);
async function resizeAndSaveImage(
imageBuffer: Buffer,
filename: string,
): Promise<void> {
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> => {
void _opts;
await fastify.register(multipart);
fastify.post(
'/api/icons/set',
{
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',
);
}
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;

21
src/icons/src/run.ts Normal file
View file

@ -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();

5
src/icons/tsconfig.json Normal file
View file

@ -0,0 +1,5 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {},
"include": ["src/**/*.ts"]
}

54
src/icons/vite.config.js Normal file
View file

@ -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
},
});

View file

@ -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",

406
src/pnpm-lock.yaml generated
View file

@ -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:

View file

@ -9,3 +9,4 @@ onlyBuiltDependencies:
- core-js
- esbuild
- protobufjs
- sharp

View file

@ -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',
@ -279,6 +291,14 @@ 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 * 60);
this.fastify.log.info('new paused game \'' + gameId + '\'');
return gameId;
}
@ -438,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;
@ -573,7 +599,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', {

View file

@ -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();
}

View file

@ -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",

View file

@ -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<typeof AddFriendRes>;
const AddFriendParams = Type.Object({
user: Type.String(),
});
export type AddFriendParams = Static<typeof AddFriendParams>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
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;

View file

@ -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<typeof ListFriendRes>;
const RemoveFriendParams = Type.Object({
user: Type.String(),
});
export type RemoveFriendParams = Static<typeof RemoveFriendParams>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
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;

View file

@ -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<typeof RemoveFriendRes>;
const RemoveFriendParams = Type.Object({
user: Type.String(),
});
export type RemoveFriendParams = Static<typeof RemoveFriendParams>;
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
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;