summaryrefslogtreecommitdiff
path: root/rushs/eplace
diff options
context:
space:
mode:
authorMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:08:27 +0200
committerMartial Simon <msimon_fr@hotmail.com>2025-09-15 01:08:27 +0200
commitc9b6b9a5ca082fe7c1b6f58d7713f785a9eb6a5c (patch)
tree3e4f42f93c7ae89a364e4d51fff6e5cec4e55fa9 /rushs/eplace
add: graphs et rushs
Diffstat (limited to 'rushs/eplace')
-rw-r--r--rushs/eplace/.env8
-rw-r--r--rushs/eplace/.gitignore24
-rw-r--r--rushs/eplace/.prettierrc.js6
-rw-r--r--rushs/eplace/eslint.config.mjs71
-rw-r--r--rushs/eplace/package.json31
-rw-r--r--rushs/eplace/public/default-avatar.pngbin0 -> 24739 bytes
-rw-r--r--rushs/eplace/public/default-icon.pngbin0 -> 29737 bytes
-rw-r--r--rushs/eplace/public/favicon.icobin0 -> 590 bytes
-rw-r--r--rushs/eplace/public/selector.svg14
-rw-r--r--rushs/eplace/server/.env36
-rw-r--r--rushs/eplace/server/config/default-canvas-250.txt1
-rw-r--r--rushs/eplace/server/config/default-canvas-50.txtbin0 -> 1563 bytes
-rw-r--r--rushs/eplace/server/config/rate-limits.config.json54
-rw-r--r--rushs/eplace/server/config/rooms.config.json39
-rw-r--r--rushs/eplace/server/docker-compose.yml56
-rw-r--r--rushs/eplace/server/openapi/openapi.json5064
-rw-r--r--rushs/eplace/src/components/debug.html30
-rw-r--r--rushs/eplace/src/components/notifications/index.html12
-rw-r--r--rushs/eplace/src/components/rooms/index.html10
-rw-r--r--rushs/eplace/src/components/rooms/message.html8
-rw-r--r--rushs/eplace/src/components/rooms/upsert.html70
-rw-r--r--rushs/eplace/src/components/rooms/user-event.html3
-rw-r--r--rushs/eplace/src/components/students/update.html31
-rw-r--r--rushs/eplace/src/pages/complete/epita/index.html12
-rw-r--r--rushs/eplace/src/pages/complete/epita/index.js53
-rw-r--r--rushs/eplace/src/pages/debug.js34
-rw-r--r--rushs/eplace/src/pages/index.css78
-rw-r--r--rushs/eplace/src/pages/index.html173
-rw-r--r--rushs/eplace/src/pages/index.js10
-rw-r--r--rushs/eplace/src/pages/styles.less905
-rw-r--r--rushs/eplace/src/pages/utils.js95
-rw-r--r--rushs/eplace/src/rooms/canvas/index.js96
-rw-r--r--rushs/eplace/src/rooms/canvas/utils.js432
-rw-r--r--rushs/eplace/src/rooms/chat/index.js4
-rw-r--r--rushs/eplace/src/rooms/chat/utils.js6
-rw-r--r--rushs/eplace/src/rooms/index.js50
-rw-r--r--rushs/eplace/src/rooms/utils.js6
-rw-r--r--rushs/eplace/src/students/index.js19
-rw-r--r--rushs/eplace/src/students/utils.js5
-rw-r--r--rushs/eplace/src/utils/auth.js124
-rw-r--r--rushs/eplace/src/utils/notify.js57
-rw-r--r--rushs/eplace/src/utils/rateLimits.js3
-rw-r--r--rushs/eplace/src/utils/streams.js108
-rw-r--r--rushs/eplace/vite.config.js41
-rw-r--r--rushs/eplace/yarn.lock1957
45 files changed, 9836 insertions, 0 deletions
diff --git a/rushs/eplace/.env b/rushs/eplace/.env
new file mode 100644
index 0000000..f372047
--- /dev/null
+++ b/rushs/eplace/.env
@@ -0,0 +1,8 @@
+VITE_HOST="localhost"
+VITE_PORT="8080"
+VITE_URL="http://${VITE_HOST}:${VITE_PORT}"
+
+#VITE_API_URL="http://localhost:3333"
+VITE_API_URL="https://eplace.assistants.epita.fr"
+VITE_AUTH_URL="https://cri.epita.fr"
+VITE_CLIENT_ID="assistants-atelier-js"
diff --git a/rushs/eplace/.gitignore b/rushs/eplace/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/rushs/eplace/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/rushs/eplace/.prettierrc.js b/rushs/eplace/.prettierrc.js
new file mode 100644
index 0000000..fcc77f6
--- /dev/null
+++ b/rushs/eplace/.prettierrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ tabWidth: 4,
+ useTabs: false, // Use spaces instead of defaults tabs
+ semi: true, // Force semilicons
+ printWidth: 80, // Max width
+};
diff --git a/rushs/eplace/eslint.config.mjs b/rushs/eplace/eslint.config.mjs
new file mode 100644
index 0000000..a315e71
--- /dev/null
+++ b/rushs/eplace/eslint.config.mjs
@@ -0,0 +1,71 @@
+import jestPlugin from "eslint-plugin-jest";
+import globals from "globals";
+import prettierPlugin from "eslint-plugin-prettier"; // Import the Prettier plugin
+import eslintComments from "eslint-plugin-eslint-comments";
+import js from "@eslint/js";
+
+const cleanGlobals = (obj) =>
+ Object.fromEntries(
+ Object.entries(obj).map(([key, val]) => [key.trim(), val]),
+ );
+
+export default [
+ js.configs.recommended, // Nice defaults rules
+ {
+ files: ["**/*.js", "**/*.mjs"], // Apply to .js and .mjs files
+ languageOptions: {
+ sourceType: "module",
+ globals: {
+ AudioWorkletGlobalScope: "readonly",
+ ...cleanGlobals(globals.node),
+ ...cleanGlobals(jestPlugin.environments.globals.globals),
+ ...cleanGlobals(globals.browser),
+ },
+ },
+ plugins: {
+ prettier: prettierPlugin, // Add Prettier plugin correctly
+ jest: jestPlugin, // Jest plugin
+ "eslint-comments": eslintComments, // Add plugin to detect cheats
+ },
+ rules: {
+ "eslint-comments/no-use": ["error", { allow: [] }], // Disallow disable rules
+ curly: ["error", "all"], // Enforce curlies in conditionnal blocks
+ "brace-style": ["error", "1tbs"],
+ "max-statements-per-line": ["error", { max: 1 }],
+ semi: ["error", "always"], // Enforce semicolons
+ "prefer-const": "error", // Prefer const over let
+ "no-undef": "error", // Detect definition
+ "no-unused-vars": [
+ "error",
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
+ ],
+ // Detect error of line width (but can't fix them without the prettier)
+ "max-len": [
+ "error",
+ {
+ code: 80,
+ tabWidth: 4,
+ ignoreComments: true,
+ ignoreStrings: true,
+ ignoreTemplateLiterals: true,
+ ignoreRegExpLiterals: true,
+ },
+ ], // More rules to complete with the existants in .prettierrc.js file
+ "prettier/prettier": ["error"], // Enforce Prettier formatting
+ "padding-line-between-statements": [
+ // Create nice looking paddings between statements
+ "error",
+ {
+ blankLine: "always",
+ prev: ["const", "let", "var", "if", "for", "while", "do"],
+ next: "*",
+ },
+ {
+ blankLine: "any",
+ prev: ["const", "let", "var"],
+ next: ["const", "let", "var"],
+ },
+ ], // Requires blank lines between the given 2 kinds of statements
+ },
+ },
+];
diff --git a/rushs/eplace/package.json b/rushs/eplace/package.json
new file mode 100644
index 0000000..fb69f09
--- /dev/null
+++ b/rushs/eplace/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "eplace-client",
+ "private": true,
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "vite",
+ "debug": "vite --mode debug"
+ },
+ "devDependencies": {
+ "eslint": "^9.25.1",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-config-standard": "^17.0.0",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-n": "^15.0.0",
+ "eslint-plugin-promise": "^6.0.0",
+ "less": "^4.1.3",
+ "vite": "^4.2.0"
+ },
+ "dependencies": {
+ "axios": "^1.4.0",
+ "eslint-plugin-eslint-comments": "^3.2.0",
+ "eslint-plugin-jest": "^28.11.0",
+ "eslint-plugin-prettier": "^5.2.6",
+ "jquery": "^3.6.4",
+ "jwt-decode": "^3.1.2",
+ "node-fetch": "^3.3.1",
+ "prettier": "^3.5.3",
+ "socket.io-client": "^4.6.1",
+ "uuid": "^9.0.0"
+ }
+}
diff --git a/rushs/eplace/public/default-avatar.png b/rushs/eplace/public/default-avatar.png
new file mode 100644
index 0000000..7c0db88
--- /dev/null
+++ b/rushs/eplace/public/default-avatar.png
Binary files differ
diff --git a/rushs/eplace/public/default-icon.png b/rushs/eplace/public/default-icon.png
new file mode 100644
index 0000000..5dc5a4f
--- /dev/null
+++ b/rushs/eplace/public/default-icon.png
Binary files differ
diff --git a/rushs/eplace/public/favicon.ico b/rushs/eplace/public/favicon.ico
new file mode 100644
index 0000000..10b730f
--- /dev/null
+++ b/rushs/eplace/public/favicon.ico
Binary files differ
diff --git a/rushs/eplace/public/selector.svg b/rushs/eplace/public/selector.svg
new file mode 100644
index 0000000..2076e95
--- /dev/null
+++ b/rushs/eplace/public/selector.svg
@@ -0,0 +1,14 @@
+<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" height="120%" width="120%">
+ <g stroke-opacity=".6" stroke-width="2">
+ <g stroke="#000">
+ <path d="m3 9v-6h6"></path>
+ <path d="m14.9999 3h6v6"></path>
+ <path d="m20.9999 15.0001v6h-6"></path>
+ <path d="m9 21.0001h-6v-6"></path>
+ </g>
+ <path d="m1 9v-8h8" stroke="#fff"></path>
+ <path d="m15 1h8v8" stroke="#fff"></path>
+ <path d="m23 15v8h-8" stroke="#fff"></path>
+ <path d="m9 23h-8v-8" stroke="#fff"></path>
+ </g>
+</svg>
diff --git a/rushs/eplace/server/.env b/rushs/eplace/server/.env
new file mode 100644
index 0000000..7ff72e7
--- /dev/null
+++ b/rushs/eplace/server/.env
@@ -0,0 +1,36 @@
+# Environment variables declared in this file are automatically made available to Prisma.
+# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
+
+# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
+# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
+
+SERVER_PORT=3333
+WSS_PORT=3334
+
+POSTGRES_USER="postgres"
+POSTGRES_PASSWORD="postgres"
+POSTGRES_HOST="postgres"
+POSTGRES_PORT=5432
+POSTGRES_DB="eplace"
+POSTGRES_SCHEMA="public"
+DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=${POSTGRES_SCHEMA}"
+
+
+REDIS_HOST="redis"
+REDIS_PORT=6379
+
+PUBLIC_API_URL="http://localhost:3333/api"
+JWKS_URI="https://cri.epita.fr/jwks"
+# DO NOT ERASE THIS UID, IT IS USE AS DEFAULT USER TO SETUP ROOMs, PIXELS
+# CHECK README FOR MORE DETAILS
+# You can add more admin uids by separating them with a comma
+# Example: 9361,9362,9363
+ADMIN_UID_LIST="9361"
+
+
+RATE_LIMITS_CONFIG_PATH="./config/rate-limits.config.json"
+ROOMS_CONFIG_PATH="./config/rooms.config.json"
+
+DISABLE_ADMIN_PREVILEGES="true"
+DISABLE_AUTH="false"
+DISABLE_RATE_LIMITING="false"
diff --git a/rushs/eplace/server/config/default-canvas-250.txt b/rushs/eplace/server/config/default-canvas-250.txt
new file mode 100644
index 0000000..9e52292
--- /dev/null
+++ b/rushs/eplace/server/config/default-canvas-250.txt
@@ -0,0 +1 @@
+B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B cB„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!BˆcÆ0„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„CÆ1ŒaB„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!BcÆ1ŒcÆ!!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!†1ŒcÆ1ŒcÆ1ŒcÆ!!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„"Æ1ŒcÆ1ŒcÆ1ŒcÆ1Œc‚„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„CÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒaB„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!BˆcÆ1ŒcÆ1Œb‚„"„1ŒcÆ1ŒcÆ „!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!BˆcÆ1ŒcÆ „!B„!B„!D1ŒcÆ1ŒcB„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!BˆcÆ1Œc‚„!B„C„!B„!Æ1ŒcÆ0„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!BˆcÆ1ŒcB„!B„#Æ1ˆ!B„!BˆcÆ1ŒcB„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„cÆ1ŒbB„!B„!Æ1ŒcB„!B„!F1ŒcÆ „!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„CÆ1ŒaB„!B„!Æ1ŒcÆ„!B„!B„CÆ1ŒaB„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„#Æ1ŒaB„!B„!B1ŒaÆ1ˆ!B„!B„!D1ŒcÄ„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„"Æ1ŒbB„!B„!BŒcBŒcB„!B„!B„cÆ1Œ!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!Æ1ŒbB„!B„!B„!„c„!B„!B„!F1Œc‚„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!Æ1ŒcB„!B„!B„!B„!Æ„!B„!B„!BˆcÆ1„!B„!B„!BˆBB„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!BˆcÆ1ŒcB„!B„!B„!B„!B„!B„!B„!B„!Æ1ŒcÆ0„!B„!BˆcÆ„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„cÆ1ŒcB„!B„!B„!B„!B„!B„!B„!B„!BŒcÆ1ŒbB„!BˆcÆ1Œ!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„"Æ1Œc„!BŒcÄ„!B„!B„!B„!B„!B„!B„"Æ1Œc„!B„cÆ1ŒcB„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!Æ1ŒcÄ„!B„cÆ1Œ!B„!B„!B„!B„!B1„!B„!B1ŒcÆ1!B„#Æ1ŒcÆ„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B1ŒcÆ „!B„"Æ1ŒcB„!B„!B„!B„!F1ŒcB„!B„cÆ1ŒAB„"Æ1ŒaÆ1!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!BŒcÆ1!B„!Æ1ˆCÆ„!B„!B„!B„!B1ŒcÆ0„!B„!Æ1ŒcB„!Æ1Œb
diff --git a/rushs/eplace/server/config/default-canvas-50.txt b/rushs/eplace/server/config/default-canvas-50.txt
new file mode 100644
index 0000000..ff61da9
--- /dev/null
+++ b/rushs/eplace/server/config/default-canvas-50.txt
Binary files differ
diff --git a/rushs/eplace/server/config/rate-limits.config.json b/rushs/eplace/server/config/rate-limits.config.json
new file mode 100644
index 0000000..aeb1cd2
--- /dev/null
+++ b/rushs/eplace/server/config/rate-limits.config.json
@@ -0,0 +1,54 @@
+{
+ "testsLimiter": {
+ "limit": 10,
+ "interval": 1
+ },
+ "getCanvasLimiter": {
+ "limit": 10,
+ "interval": 1
+ },
+ "getPixelLimiter": {
+ "limit": 10,
+ "interval": 1
+ },
+ "placePixelLimiter": {
+ "limit": 1,
+ "interval": 30
+ },
+ "getRoomsLimiter": {
+ "limit": 10,
+ "interval": 1
+ },
+ "getRoomConfigLimiter": {
+ "limit": 10,
+ "interval": 1
+ },
+ "createRoomLimiter": {
+ "limit": 1,
+ "interval": 300
+ },
+ "updateRoomLimiter": {
+ "limit": 1,
+ "interval": 1
+ },
+ "deleteRoomLimiter": {
+ "limit": 2,
+ "interval": 1
+ },
+ "getStudentLimiter": {
+ "limit": 10,
+ "interval": 1
+ },
+ "updateStudentLimiter": {
+ "limit": 1,
+ "interval": 1
+ },
+ "sendMessageLimiter": {
+ "limit": 1,
+ "interval": 1
+ },
+ "reportRoomLimiter": {
+ "limit": 1,
+ "interval": 5
+ }
+}
diff --git a/rushs/eplace/server/config/rooms.config.json b/rushs/eplace/server/config/rooms.config.json
new file mode 100644
index 0000000..b50080e
--- /dev/null
+++ b/rushs/eplace/server/config/rooms.config.json
@@ -0,0 +1,39 @@
+{
+ "maxRoomsCreatedPerUser": 3,
+ "rooms": {
+ "default": {
+ "metadata": {
+ "canvasDimensions": 50
+ },
+ "settings": {
+ "roomColors": "#ffffff,#d4d7d9,#898d90,#515252,#000000,#fe4500,#fea800,#fed634,#01a268,#7eed56,#2350a4,#3690ea,#51e9f4,#811f9f,#b44bc0,#ff99aa,#9c6925",
+ "defaultCanvas": "config/default-canvas-50.txt"
+ }
+ },
+ "epi-place": {
+ "metadata": {
+ "name": "epi/place",
+ "slug": "epi-place",
+ "description": "Le Roi de la malice est passé par là",
+ "canvasDimensions": 250,
+ "iconURL": "https://media.tenor.com/XUHq8pN_maQAAAAi/puffer-fish-fish.gif",
+ "isPublic": true
+ },
+ "settings": {
+ "roomColors": "#ffffff,#d4d7d9,#898d90,#515252,#6c001a,#be0039,#fe4500,#fea800,#fed634,#fff8b8,#01a268,#00cc78,#7eed56,#02756f,#019eaa,#00ccbf,#2350a4,#3690ea,#51e9f4,#493ac1,#6a5cff,#94b3ff,#811f9f,#b44bc0,#e4aaff,#de107f,#ff3981,#ff99aa,#6d482f,#9c6925,#ffb470,#000000",
+ "defaultCanvas": "config/default-canvas-250.txt"
+ }
+ },
+ "test": {
+ "metadata": {
+ "name": "Test Room",
+ "description": "A room small enough to test things out",
+ "canvasDimensions": 10,
+ "isPublic": true
+ },
+ "settings": {
+ "roomColors": "#ffffff,#d4d7d9,#898d90,#515252,#000000,#fe4500,#fea800,#fed634,#01a268,#7eed56,#2350a4,#3690ea,#51e9f4,#811f9f,#b44bc0,#ff99aa,#9c6925"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/rushs/eplace/server/docker-compose.yml b/rushs/eplace/server/docker-compose.yml
new file mode 100644
index 0000000..f0419b9
--- /dev/null
+++ b/rushs/eplace/server/docker-compose.yml
@@ -0,0 +1,56 @@
+version: '3.9'
+services:
+ postgres:
+ image: registry.cri.epita.fr/ing/assistants/public/registry/postgres:15.2-alpine
+ container_name: postgres
+ restart: always
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: eplace
+ expose:
+ - 5432
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ # Proper docker-compose would use named networks
+ # networks:
+ # - postgres-network
+ redis:
+ image: registry.cri.epita.fr/ing/assistants/public/registry/redis:7.0.9-alpine
+ container_name: redis
+ restart: always
+ expose:
+ - 6379
+ volumes:
+ - redis-data:/data
+ # Proper docker-compose would use named networks
+ # networks:
+ # - redis-network
+ eplace:
+ image: registry.cri.epita.fr/ing/assistants/public/registry/eplace:latest
+ container_name: eplace
+ restart: always
+ environment:
+ NODE_ENV: production
+ volumes:
+ - ./config:/usr/src/app/config
+ - type: 'bind'
+ source: './.env'
+ target: '/usr/src/app/.env'
+ ports:
+ - 3000:3000
+ - 3333:3333
+ # Proper docker-compose would use named networks
+ # networks:
+ # - postgres-network
+ # - redis-network
+ depends_on:
+ - postgres
+ - redis
+volumes:
+ postgres-data:
+ redis-data:
+ # Proper docker-compose would use named networks
+ # networks:
+ # postgres-network:
+ # redis-network:
diff --git a/rushs/eplace/server/openapi/openapi.json b/rushs/eplace/server/openapi/openapi.json
new file mode 100644
index 0000000..4f0dd77
--- /dev/null
+++ b/rushs/eplace/server/openapi/openapi.json
@@ -0,0 +1,5064 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "E/PLACE API",
+ "description": "Publicly available API for E/PLACE",
+ "version": "1.0.0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:3333/api"
+ }
+ ],
+ "paths": {
+ "/status": {
+ "get": {
+ "operationId": "status",
+ "summary": "Get the status of the API",
+ "description": "Get the status of the API",
+ "tags": [
+ "Misc"
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/reload-config": {
+ "put": {
+ "operationId": "reloadConfig",
+ "summary": "Reload the config",
+ "description": "Reload the config",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "reloadLocally": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "reloadLocally"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/update-event/{slug}": {
+ "post": {
+ "operationId": "changeEvent",
+ "summary": "Update the event of a room",
+ "description": "Update the event of a room",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "event": {
+ "type": "string",
+ "enum": [
+ null,
+ "EVEN_OR_ODD",
+ "EVEN_OR_ODD_DEFAULT_CANVAS",
+ "INITIAL_DEFAULT_CANVAS",
+ "RANDOM",
+ "GROUPS",
+ "VOID"
+ ],
+ "nullable": true,
+ "default": null
+ },
+ "radius": {
+ "type": "number"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/update-rate-limits/{slug}": {
+ "post": {
+ "operationId": "updateRateLimits",
+ "summary": "Update the rate limits",
+ "description": "Update the rate limits for the API",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "rateLimitName": {
+ "type": "string",
+ "enum": [
+ "testsLimiter",
+ "getCanvasLimiter",
+ "getPixelLimiter",
+ "placePixelLimiter",
+ "getRoomsLimiter",
+ "getRoomConfigLimiter",
+ "createRoomLimiter",
+ "updateRoomLimiter",
+ "deleteRoomLimiter",
+ "getStudentLimiter",
+ "updateStudentLimiter",
+ "sendMessageLimiter",
+ "reportRoomLimiter"
+ ]
+ },
+ "limit": {
+ "type": "number",
+ "minimum": 1
+ },
+ "interval": {
+ "type": "number",
+ "minimum": 0
+ }
+ },
+ "required": [
+ "rateLimitName",
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/tests/error": {
+ "get": {
+ "operationId": "tests-error",
+ "summary": "Return a server error",
+ "description": "Always return 500 Internal Server Error",
+ "tags": [
+ "Tests"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/tests/invalid-token": {
+ "post": {
+ "operationId": "tests-invalid-token",
+ "summary": "Return an invalid token error",
+ "description": "Always return 401 Unauthorized \"Invalid token\"",
+ "tags": [
+ "Tests"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/tests/expired-token": {
+ "post": {
+ "operationId": "tests-expired-token",
+ "summary": "Return a token expired error",
+ "description": "Always return 401 Unauthorized \"Token expired\"",
+ "tags": [
+ "Tests"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/tests/too-many-requests": {
+ "get": {
+ "operationId": "tests-too-many-requests",
+ "summary": "Return a too many requests error",
+ "description": "Always return 429 Too Many Requests",
+ "tags": [
+ "Tests"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/students": {
+ "get": {
+ "operationId": "students-getStudents",
+ "summary": "Get all students",
+ "description": "Get all students",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "uid": {
+ "type": "number"
+ },
+ "login": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "uid",
+ "login"
+ ],
+ "additionalProperties": false
+ },
+ "description": "An array of students with their UID and login"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/students/{id}": {
+ "get": {
+ "operationId": "students-getStudent",
+ "summary": "Get a student by UID or login",
+ "description": "Get a student by UID or login",
+ "tags": [
+ "Students (Mandatory)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The UID or login of the student"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "groups": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "ADMIN",
+ "YAKA",
+ "ING1_LYON",
+ "ING1_PARIS",
+ "ING1_RENNES",
+ "ING1_STRASBOURG",
+ "ING1_TOULOUSE"
+ ]
+ }
+ },
+ "uid": {
+ "type": "number"
+ },
+ "login": {
+ "type": "string"
+ },
+ "avatarURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "quote": {
+ "type": "string",
+ "nullable": true
+ },
+ "currentRoomSlug": {
+ "type": "string",
+ "nullable": true
+ },
+ "banUntil": {
+ "type": "string",
+ "format": "date-time",
+ "nullable": true
+ }
+ },
+ "required": [
+ "groups",
+ "uid",
+ "login",
+ "avatarURL",
+ "quote",
+ "currentRoomSlug",
+ "banUntil"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "put": {
+ "operationId": "students-updateStudent",
+ "summary": "Update a student by UID or login",
+ "description": "Update a student by UID or login",
+ "tags": [
+ "Students (Optional)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "avatarURL": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ],
+ "description": "The URL of your avatar"
+ },
+ "quote": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ],
+ "description": "Your quote"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "Your student UID or login"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "groups": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "ADMIN",
+ "YAKA",
+ "ING1_LYON",
+ "ING1_PARIS",
+ "ING1_RENNES",
+ "ING1_STRASBOURG",
+ "ING1_TOULOUSE"
+ ]
+ }
+ },
+ "uid": {
+ "type": "number"
+ },
+ "login": {
+ "type": "string"
+ },
+ "avatarURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "quote": {
+ "type": "string",
+ "nullable": true
+ },
+ "currentRoomSlug": {
+ "type": "string",
+ "nullable": true
+ },
+ "banUntil": {
+ "type": "string",
+ "format": "date-time",
+ "nullable": true
+ }
+ },
+ "required": [
+ "groups",
+ "uid",
+ "login",
+ "avatarURL",
+ "quote",
+ "currentRoomSlug",
+ "banUntil"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/students/{id}/ban": {
+ "post": {
+ "operationId": "students-banStudent",
+ "summary": "Ban a student by UID or login",
+ "description": "Ban a student by UID or by login. If no date is provided, the student will be unbanned.",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "banUntil": {
+ "type": "string",
+ "description": "The date until the student is banned"
+ },
+ "reason": {
+ "type": "string",
+ "description": "The reason of the ban"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The UID or login of the student"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "groups": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "ADMIN",
+ "YAKA",
+ "ING1_LYON",
+ "ING1_PARIS",
+ "ING1_RENNES",
+ "ING1_STRASBOURG",
+ "ING1_TOULOUSE"
+ ]
+ }
+ },
+ "uid": {
+ "type": "number"
+ },
+ "login": {
+ "type": "string"
+ },
+ "avatarURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "quote": {
+ "type": "string",
+ "nullable": true
+ },
+ "currentRoomSlug": {
+ "type": "string",
+ "nullable": true
+ },
+ "banUntil": {
+ "type": "string",
+ "format": "date-time",
+ "nullable": true
+ }
+ },
+ "required": [
+ "groups",
+ "uid",
+ "login",
+ "avatarURL",
+ "quote",
+ "currentRoomSlug",
+ "banUntil"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms": {
+ "get": {
+ "operationId": "rooms-getRooms",
+ "summary": "Get all rooms",
+ "description": "List all the rooms available to the student",
+ "tags": [
+ "Rooms (Optional)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "List of available rooms",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Room"
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "operationId": "rooms-createRoom",
+ "summary": "Create a room",
+ "description": "Create a new room",
+ "tags": [
+ "Rooms (Optional)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the room"
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ],
+ "description": "The description of the room"
+ },
+ "iconURL": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ],
+ "description": "The URL of the room's icon"
+ },
+ "isPublic": {
+ "type": "boolean",
+ "description": "Whether the room is public or not. Defaults to false"
+ },
+ "studentsWhitelist": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "description": "The list of students allowed to join the room. Effective only if isPublic is false. Accepts both UIDs and logins. Defaults to an empty array"
+ },
+ "studentsBlacklist": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "description": "The list of students not allowed to join the room. Effective only if isPublic is true. Accepts both UIDs and logins. Defaults to an empty array"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true
+ },
+ "iconURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "canvasDimensions": {
+ "type": "number"
+ },
+ "isPublic": {
+ "type": "boolean"
+ },
+ "studentsWhitelist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "studentsBlacklist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "ownerUid": {
+ "type": "number"
+ },
+ "password": {
+ "type": "string",
+ "nullable": true
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "deleted": {
+ "type": "boolean"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "slug",
+ "name",
+ "description",
+ "iconURL",
+ "canvasDimensions",
+ "isPublic",
+ "studentsWhitelist",
+ "studentsBlacklist",
+ "ownerUid",
+ "password",
+ "hidden",
+ "deleted",
+ "createdAt"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/{slug}": {
+ "put": {
+ "operationId": "rooms-updateRoom",
+ "summary": "Update a room",
+ "description": "Update a room",
+ "tags": [
+ "Rooms (Optional)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the room"
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ],
+ "description": "The description of the room"
+ },
+ "iconURL": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ],
+ "description": "The URL of the room's icon"
+ },
+ "isPublic": {
+ "type": "boolean",
+ "description": "Whether the room is public or not. Defaults to false"
+ },
+ "studentsWhitelist": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "description": "The list of students allowed to join the room. Effective only if isPublic is false. Accepts both UIDs and logins. Defaults to an empty array"
+ },
+ "studentsBlacklist": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "description": "The list of students not allowed to join the room. Effective only if isPublic is true. Accepts both UIDs and logins. Defaults to an empty array"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true
+ },
+ "iconURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "canvasDimensions": {
+ "type": "number"
+ },
+ "isPublic": {
+ "type": "boolean"
+ },
+ "studentsWhitelist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "studentsBlacklist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "ownerUid": {
+ "type": "number"
+ },
+ "password": {
+ "type": "string",
+ "nullable": true
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "deleted": {
+ "type": "boolean"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "slug",
+ "name",
+ "description",
+ "iconURL",
+ "canvasDimensions",
+ "isPublic",
+ "studentsWhitelist",
+ "studentsBlacklist",
+ "ownerUid",
+ "password",
+ "hidden",
+ "deleted",
+ "createdAt"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "operationId": "rooms-deleteRoom",
+ "summary": "Delete a room",
+ "description": "Delete a room",
+ "tags": [
+ "Rooms (Optional)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/{slug}/config": {
+ "get": {
+ "operationId": "rooms-getRoomConfig",
+ "summary": "Get the room config",
+ "description": "Get the room config",
+ "tags": [
+ "Rooms (Mandatory)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "metadata": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ]
+ },
+ "iconURL": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ]
+ },
+ "canvasDimensions": {
+ "type": "number"
+ },
+ "isPublic": {
+ "type": "boolean"
+ },
+ "studentsWhitelist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "studentsBlacklist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "ownerUid": {
+ "type": "number"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "slug",
+ "name",
+ "description",
+ "iconURL",
+ "canvasDimensions",
+ "isPublic",
+ "ownerUid",
+ "createdAt"
+ ],
+ "additionalProperties": false
+ },
+ "settings": {
+ "type": "object",
+ "properties": {
+ "roomColors": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "roomColors"
+ ],
+ "additionalProperties": false
+ },
+ "rateLimits": {
+ "type": "object",
+ "properties": {
+ "testsLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getCanvasLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getPixelLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "placePixelLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getRoomsLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getRoomConfigLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "createRoomLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "updateRoomLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "deleteRoomLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getStudentLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "updateStudentLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "sendMessageLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "reportRoomLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "testsLimiter",
+ "getCanvasLimiter",
+ "getPixelLimiter",
+ "placePixelLimiter",
+ "getRoomsLimiter",
+ "getRoomConfigLimiter",
+ "createRoomLimiter",
+ "updateRoomLimiter",
+ "deleteRoomLimiter",
+ "getStudentLimiter",
+ "updateStudentLimiter",
+ "sendMessageLimiter",
+ "reportRoomLimiter"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "metadata",
+ "settings",
+ "rateLimits"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "operationId": "rooms-changeRoomConfig",
+ "summary": "Load another json room config",
+ "description": "Load another json room config",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "settings": {
+ "type": "object",
+ "properties": {
+ "roomColors": {
+ "type": "string",
+ "description": "The colors of the room"
+ }
+ },
+ "additionalProperties": false,
+ "description": "The settings to change"
+ },
+ "events": {
+ "type": "object",
+ "properties": {
+ "colorationEvent": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "The type of the coloration event"
+ },
+ "groups": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "The groups of the coloration event"
+ },
+ "colorsSubSet": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ },
+ "description": "The colors subset of the coloration event"
+ }
+ },
+ "required": [
+ "type"
+ ],
+ "additionalProperties": false
+ },
+ "radiusEvent": {
+ "type": "object",
+ "properties": {
+ "radius": {
+ "type": "number",
+ "description": "The radius of the event"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "rateLimitsOverride": {
+ "type": "object",
+ "properties": {
+ "testsLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getCanvasLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getPixelLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "placePixelLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getRoomsLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getRoomConfigLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "createRoomLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "updateRoomLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "deleteRoomLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "getStudentLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "updateStudentLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "sendMessageLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ },
+ "reportRoomLimiter": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "limit",
+ "interval"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "testsLimiter",
+ "getCanvasLimiter",
+ "getPixelLimiter",
+ "placePixelLimiter",
+ "getRoomsLimiter",
+ "getRoomConfigLimiter",
+ "createRoomLimiter",
+ "updateRoomLimiter",
+ "deleteRoomLimiter",
+ "getStudentLimiter",
+ "updateStudentLimiter",
+ "sendMessageLimiter",
+ "reportRoomLimiter"
+ ],
+ "additionalProperties": false,
+ "description": "The rate limits to override"
+ }
+ },
+ "required": [
+ "events"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/{slug}/report": {
+ "post": {
+ "operationId": "rooms-reportRoom",
+ "summary": "Report a room",
+ "description": "Report a room",
+ "tags": [
+ "Misc"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "reason": {
+ "type": "string",
+ "description": "The reason for the report"
+ }
+ },
+ "required": [
+ "reason"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/yeet": {
+ "post": {
+ "operationId": "rooms-yeetFromRoom",
+ "summary": "Yeet a student from a room",
+ "description": "Yeet a student from a room",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string",
+ "description": "The slug of the room. If empty, all rooms will be yeeted"
+ },
+ "studentsList": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "description": "The list of students to yeet. If empty, all students will be yeeted"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/{slug}/canvas": {
+ "get": {
+ "operationId": "rooms-canvas-getCanvas",
+ "summary": "Get the canvas of a room",
+ "description": "Get the canvas of a room",
+ "tags": [
+ "Rooms (Mandatory)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "pixels": {
+ "type": "string",
+ "description": "The pixels of the room"
+ }
+ },
+ "required": [
+ "pixels"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/{slug}/canvas/pixels": {
+ "get": {
+ "operationId": "rooms-canvas-getPixel",
+ "summary": "Get the pixels of a room",
+ "description": "Get the pixels of a room",
+ "tags": [
+ "Rooms (Mandatory)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ },
+ {
+ "name": "posX",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "number"
+ },
+ "description": "The X position of the pixel"
+ },
+ {
+ "name": "posY",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "number"
+ },
+ "description": "The Y position of the pixel"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "roomSlug": {
+ "type": "string"
+ },
+ "posX": {
+ "type": "number"
+ },
+ "posY": {
+ "type": "number"
+ },
+ "color": {
+ "type": "number"
+ },
+ "timestamp": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ]
+ },
+ "placedByUid": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ]
+ }
+ },
+ "required": [
+ "roomSlug",
+ "posX",
+ "posY",
+ "color",
+ "timestamp",
+ "placedByUid"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "operationId": "rooms-canvas-placePixel",
+ "summary": "Place a pixel in a room",
+ "description": "Place a pixel in a room",
+ "tags": [
+ "Rooms (Mandatory)"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "posX": {
+ "type": "number",
+ "description": "The X position of the pixel"
+ },
+ "posY": {
+ "type": "number",
+ "description": "The Y position of the pixel"
+ },
+ "color": {
+ "type": "number",
+ "description": "The color index of the pixel"
+ }
+ },
+ "required": [
+ "posX",
+ "posY",
+ "color"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "roomSlug": {
+ "type": "string"
+ },
+ "posX": {
+ "type": "number"
+ },
+ "posY": {
+ "type": "number"
+ },
+ "color": {
+ "type": "number"
+ },
+ "timestamp": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ]
+ },
+ "placedByUid": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "enum": [
+ "null"
+ ],
+ "nullable": true
+ }
+ ]
+ }
+ },
+ "required": [
+ "roomSlug",
+ "posX",
+ "posY",
+ "color",
+ "timestamp",
+ "placedByUid"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/{slug}/canvas/resize": {
+ "put": {
+ "operationId": "rooms-canvas-resizeCanvas",
+ "summary": "Resize the canvas of a room",
+ "description": "Resize the canvas of a room",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "newCanvasDimensions": {
+ "type": "number",
+ "description": "The new size of the room"
+ }
+ },
+ "required": [
+ "newCanvasDimensions"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true
+ },
+ "iconURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "canvasDimensions": {
+ "type": "number"
+ },
+ "isPublic": {
+ "type": "boolean"
+ },
+ "studentsWhitelist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "studentsBlacklist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "ownerUid": {
+ "type": "number"
+ },
+ "password": {
+ "type": "string",
+ "nullable": true
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "deleted": {
+ "type": "boolean"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "slug",
+ "name",
+ "description",
+ "iconURL",
+ "canvasDimensions",
+ "isPublic",
+ "studentsWhitelist",
+ "studentsBlacklist",
+ "ownerUid",
+ "password",
+ "hidden",
+ "deleted",
+ "createdAt"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/{slug}/canvas/reset": {
+ "put": {
+ "operationId": "rooms-canvas-resetCanvas",
+ "summary": "Reset the canvas of a room",
+ "description": "Reset the canvas of a room",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "resetOnDefault": {
+ "type": "boolean",
+ "description": "Reset the canvas on the default image if exists"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true
+ },
+ "iconURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "canvasDimensions": {
+ "type": "number"
+ },
+ "isPublic": {
+ "type": "boolean"
+ },
+ "studentsWhitelist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "studentsBlacklist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "ownerUid": {
+ "type": "number"
+ },
+ "password": {
+ "type": "string",
+ "nullable": true
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "deleted": {
+ "type": "boolean"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "slug",
+ "name",
+ "description",
+ "iconURL",
+ "canvasDimensions",
+ "isPublic",
+ "studentsWhitelist",
+ "studentsBlacklist",
+ "ownerUid",
+ "password",
+ "hidden",
+ "deleted",
+ "createdAt"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/rooms/{slug}/canvas/load": {
+ "post": {
+ "operationId": "rooms-canvas-loadCanvas",
+ "summary": "Load the canvas of a room",
+ "description": "Load the canvas of a room",
+ "tags": [
+ "Admin"
+ ],
+ "security": [
+ {
+ "Authorization": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "backupId": {
+ "type": "number",
+ "description": "The id of the backup"
+ }
+ },
+ "required": [
+ "backupId"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "The slug of the room"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InvalidToken": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Invalid token"
+ }
+ },
+ "TokenExpired": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Token expired"
+ }
+ },
+ "Unauthenticated": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "Unauthenticated"
+ }
+ },
+ "Banned": {
+ "value": {
+ "code": "UNAUTHORIZED",
+ "message": "You are banned"
+ }
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "NotAdmin": {
+ "value": {
+ "code": "FORBIDDEN",
+ "message": "You are not an admin"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too Many Requests",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "TooManyRequests": {
+ "value": {
+ "code": "TOO_MANY_REQUESTS",
+ "message": "Too many requests"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ },
+ "examples": {
+ "InternalServerError": {
+ "value": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "message": "An error occured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "Authorization": {
+ "type": "http",
+ "scheme": "bearer"
+ }
+ },
+ "responses": {
+ "error": {
+ "description": "Error response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "code": {
+ "type": "string"
+ },
+ "issues": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": [
+ "message",
+ "code"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ }
+ },
+ "schemas": {
+ "Room": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true
+ },
+ "iconURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "canvasDimensions": {
+ "type": "number"
+ },
+ "isPublic": {
+ "type": "boolean"
+ },
+ "studentsWhitelist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "studentsBlacklist": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "ownerUid": {
+ "type": "number"
+ },
+ "password": {
+ "type": "string",
+ "nullable": true
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "deleted": {
+ "type": "boolean"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "Student": {
+ "type": "object",
+ "properties": {
+ "groups": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "ADMIN",
+ "YAKA",
+ "ING1_LYON",
+ "ING1_PARIS",
+ "ING1_RENNES",
+ "ING1_STRASBOURG",
+ "ING1_TOULOUSE"
+ ]
+ }
+ },
+ "uid": {
+ "type": "number"
+ },
+ "login": {
+ "type": "string"
+ },
+ "avatarURL": {
+ "type": "string",
+ "nullable": true
+ },
+ "quote": {
+ "type": "string",
+ "nullable": true
+ },
+ "currentRoomSlug": {
+ "type": "string",
+ "nullable": true
+ },
+ "banUntil": {
+ "type": "string",
+ "format": "date-time",
+ "nullable": true
+ }
+ }
+ },
+ "Error": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "Misc"
+ },
+ {
+ "name": "Tests"
+ },
+ {
+ "name": "Rooms (Mandatory)"
+ },
+ {
+ "name": "Rooms (Optional)"
+ },
+ {
+ "name": "Students (Mandatory)"
+ },
+ {
+ "name": "Students (Optional)"
+ },
+ {
+ "name": "Admin"
+ }
+ ]
+} \ No newline at end of file
diff --git a/rushs/eplace/src/components/debug.html b/rushs/eplace/src/components/debug.html
new file mode 100644
index 0000000..750906d
--- /dev/null
+++ b/rushs/eplace/src/components/debug.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="stylesheet" type="text/css" media="screen" href="index.css" />
+</head>
+
+<body>
+ <div class="container">
+ <h1>Local storage</h1>
+
+ <h3>Token</h3>
+ <p id="token" class="token-text">
+ {{ token }}
+ </p>
+ <h3>Refresh Token</h3>
+ <p id="refresh_token" class="token-text">
+ {{ refresh_token }}
+ </p>
+
+ <button id="errorBtn">Generate an error response</button>
+ <button id="expiredTokenBtn">Generate an expired token response</button>
+ <button id="deleteTokenBtn">Delete token</button>
+ <button id="deleteRefreshTokenBtn">Delete refresh token</button>
+ </div>
+</body>
+
+</html>
diff --git a/rushs/eplace/src/components/notifications/index.html b/rushs/eplace/src/components/notifications/index.html
new file mode 100644
index 0000000..c211a39
--- /dev/null
+++ b/rushs/eplace/src/components/notifications/index.html
@@ -0,0 +1,12 @@
+<div class="Alert">
+ <div class="Icon">
+ <i class="fa {{icon_classes}}"></i>
+ </div>
+ <div class="AlertBody">
+ <span class="AlertTitle">{{title}}</span>
+ <span class="AlertContent">{{content}}</span>
+ </div>
+ <span class="AlertClose">
+ <i class="fa fa-times-circle"></i>
+ </span>
+</div>
diff --git a/rushs/eplace/src/components/rooms/index.html b/rushs/eplace/src/components/rooms/index.html
new file mode 100644
index 0000000..3d9f20e
--- /dev/null
+++ b/rushs/eplace/src/components/rooms/index.html
@@ -0,0 +1,10 @@
+<button class="Room">
+ <img src="{{icon_url}}" class="Avatar" />
+ <div class="TextContainer">
+ <div class="Name">
+ <i class="RoomPrivacy {{privacy_icon}}"></i>
+ <span>{{name}}</span>
+ </div>
+ <span class="RoomOwner">owner: {{owner_login}}</span>
+ </div>
+</button>
diff --git a/rushs/eplace/src/components/rooms/message.html b/rushs/eplace/src/components/rooms/message.html
new file mode 100644
index 0000000..1c35e20
--- /dev/null
+++ b/rushs/eplace/src/components/rooms/message.html
@@ -0,0 +1,8 @@
+<div class="ChatMessage">
+ <div class="MessageHeader">
+ <img src="{{avatar_url}}" class="Avatar" />
+ <span class="Login">{{message_author}}</span>
+ <span class="Time">{{sent_at}}</span>
+ </div>
+ <span class="MessageContent">{{message_content}}</span>
+</div>
diff --git a/rushs/eplace/src/components/rooms/upsert.html b/rushs/eplace/src/components/rooms/upsert.html
new file mode 100644
index 0000000..8b02b69
--- /dev/null
+++ b/rushs/eplace/src/components/rooms/upsert.html
@@ -0,0 +1,70 @@
+<div class="FormOverlay">
+ <form class="StylisedForm" id="room-upsert-form">
+ <div class="FormHeader">
+ <h2 class="FormTitle">{{form_title}}</h2>
+ </div>
+ <div class="FormItem">
+ <label for="name" class="FormLabel">Name</label>
+ <input
+ type="text"
+ class="FormInput"
+ id="name"
+ name="name"
+ placeholder="Enter name"
+ />
+ </div>
+ <div class="FormItem">
+ <label for="description" class="FormLabel">Description</label>
+ <input
+ type="text"
+ class="FormInput"
+ id="description"
+ name="description"
+ placeholder="Enter description"
+ />
+ </div>
+ <div class="FormItem">
+ <label for="icon-url" class="FormLabel">Icon URL</label>
+ <input
+ type=""
+ class="FormInput"
+ id="icon-url"
+ name="icon-url"
+ placeholder="Enter icon URL"
+ />
+ </div>
+ <div class="FormItem">
+ <label for="whitelist" class="FormLabel">Students Whitelist</label>
+ <input
+ type="text"
+ class="FormInput"
+ id="whitelist"
+ name="whitelist"
+ placeholder="Enter student's logins or UIDs, separated by commas (private rooms only)"
+ />
+ </div>
+ <div class="FormItem">
+ <label for="blacklist" class="FormLabel">Students Blacklist</label>
+ <input
+ type="text"
+ class="FormInput"
+ id="blacklist"
+ name="blacklist"
+ placeholder="Enter student's logins or UIDs, separated by commas (public rooms only)"
+ />
+ </div>
+ <div class="FormItem">
+ <label for="is-public" class="FormLabel">Is Public</label>
+ <input
+ type="checkbox"
+ class="FormInput"
+ id="is-public"
+ name="is-public"
+ />
+ </div>
+ <div class="FormButtons">
+ <button type="button" id="close-modal">Cancel</button>
+ <button type="submit">Submit</button>
+ </div>
+ </form>
+</div>
diff --git a/rushs/eplace/src/components/rooms/user-event.html b/rushs/eplace/src/components/rooms/user-event.html
new file mode 100644
index 0000000..3aec718
--- /dev/null
+++ b/rushs/eplace/src/components/rooms/user-event.html
@@ -0,0 +1,3 @@
+<div class="ChatUserEvent">
+ <span class="MessageContent">{{message_content}}</span>
+</div>
diff --git a/rushs/eplace/src/components/students/update.html b/rushs/eplace/src/components/students/update.html
new file mode 100644
index 0000000..912526e
--- /dev/null
+++ b/rushs/eplace/src/components/students/update.html
@@ -0,0 +1,31 @@
+<div class="FormOverlay">
+ <form class="StylisedForm" id="student-update-form">
+ <div class="FormHeader">
+ <h2 class="FormTitle">Update Profile</h2>
+ </div>
+ <div class="FormItem">
+ <label for="avatar-url" class="FormLabel">Avatar URL</label>
+ <input
+ type=""
+ class="FormInput"
+ id="avatar-url"
+ name="avatar-url"
+ placeholder="Enter avatar URL"
+ />
+ </div>
+ <div class="FormItem">
+ <label for="quote" class="FormLabel">Quote</label>
+ <input
+ type="text"
+ class="FormInput"
+ id="quote"
+ name="quote"
+ placeholder="Enter a quote"
+ />
+ </div>
+ <div class="FormButtons">
+ <button type="button" id="close-modal">Cancel</button>
+ <button type="submit">Submit</button>
+ </div>
+ </form>
+</div>
diff --git a/rushs/eplace/src/pages/complete/epita/index.html b/rushs/eplace/src/pages/complete/epita/index.html
new file mode 100644
index 0000000..a6ebe75
--- /dev/null
+++ b/rushs/eplace/src/pages/complete/epita/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <title>E/PLACE</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="/favicon.ico" />
+ <script type="module" src="index.js"></script>
+ </head>
+ <body></body>
+</html>
diff --git a/rushs/eplace/src/pages/complete/epita/index.js b/rushs/eplace/src/pages/complete/epita/index.js
new file mode 100644
index 0000000..ced46cf
--- /dev/null
+++ b/rushs/eplace/src/pages/complete/epita/index.js
@@ -0,0 +1,53 @@
+// FIXME: This file should handle the auth redirection
+
+// Get the code from the URL parameters and redirect to the relevant page
+const params = new URLSearchParams(window.location.search);
+
+const code = params.get("code");
+const form = new FormData();
+
+form.append("client_id", import.meta.env.VITE_CLIENT_ID);
+form.append("redirect_uri", `${import.meta.env.VITE_URL}/complete/epita/`);
+form.append("grant_type", "authorization_code");
+form.append("code", code);
+const config = {
+ method: "POST",
+ body: form,
+};
+
+export async function getToken() {
+ try {
+ const res = await fetch(
+ `${import.meta.env.VITE_URL}/auth-api/token`,
+ config,
+ );
+ const response = await res.json();
+
+ const token = response.id_token;
+
+ localStorage.setItem("token", token);
+ localStorage.setItem("refresh_token", response.refresh_token);
+ window.location.replace(import.meta.env.VITE_URL);
+ return true;
+ } catch (error) {
+ console.log(error);
+ return false;
+ }
+}
+
+if (!code) {
+ const authQueryParams = {
+ client_id: import.meta.env.VITE_CLIENT_ID,
+ scope: "epita profile picture",
+ redirect_uri: `${import.meta.env.VITE_URL}/complete/epita/`,
+ response_type: "code",
+ };
+ const url = new URL(
+ `?client_id=${authQueryParams.client_id}&scope=${authQueryParams.scope}&redirect_uri=${authQueryParams.redirect_uri}&response_type=${authQueryParams.response_type}`,
+ `${import.meta.env.VITE_AUTH_URL}/authorize`,
+ );
+
+ window.location.replace(url);
+} else {
+ getToken();
+}
diff --git a/rushs/eplace/src/pages/debug.js b/rushs/eplace/src/pages/debug.js
new file mode 100644
index 0000000..e2da5f4
--- /dev/null
+++ b/rushs/eplace/src/pages/debug.js
@@ -0,0 +1,34 @@
+import $ from "jquery";
+import debugHtml from "../components/debug.html";
+
+function refreshLocalStorage() {
+ $("#token").text(localStorage.getItem("token") ?? "N/A");
+ $("#refresh_token").text(localStorage.getItem("refresh_token") ?? "N/A");
+}
+
+if (import.meta.env.MODE === "debug") {
+ $.get(debugHtml, function (response) {
+ $("body").html(response);
+ refreshLocalStorage();
+ }).fail(function (xhr, status, error) {
+ console.error("Error fetching debug HTML:", error);
+ });
+
+ $(document).on("click", "#errorBtn", function () {
+ // Make a call to VITE_URL/tests/error using your own api call function
+ });
+
+ $(document).on("click", "#expiredTokenBtn", function () {
+ // Make a call to VITE_URL/tests/expired using your own api call function
+ });
+
+ $(document).on("click", "#deleteTokenBtn", function () {
+ localStorage.removeItem("token");
+ refreshLocalStorage();
+ });
+
+ $(document).on("click", "#deleteRefreshTokenBtn", function () {
+ localStorage.removeItem("refresh_token");
+ refreshLocalStorage();
+ });
+}
diff --git a/rushs/eplace/src/pages/index.css b/rushs/eplace/src/pages/index.css
new file mode 100644
index 0000000..6548524
--- /dev/null
+++ b/rushs/eplace/src/pages/index.css
@@ -0,0 +1,78 @@
+@import url(http://fonts.googleapis.com/css?family=Barlow:800);
+@import url(http://fonts.googleapis.com/css?family=Montserrat:400,700);
+
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif, Barlow;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ justify-content: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+ margin: 0;
+}
+
+h2 {
+ margin: 0;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+input {
+ border: none;
+}
+.token-text {
+ max-width: 10ch;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
diff --git a/rushs/eplace/src/pages/index.html b/rushs/eplace/src/pages/index.html
new file mode 100644
index 0000000..d52232b
--- /dev/null
+++ b/rushs/eplace/src/pages/index.html
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <title>E/PLACE</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="/favicon.ico" />
+ <link rel="stylesheet" type="text/css" media="screen" href="index.css" />
+ <link rel="stylesheet/less" type="text/css" href="styles.less" />
+ <link
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
+ rel="stylesheet"
+ />
+ <script src="https://cdn.jsdelivr.net/npm/less"></script>
+ <script type="module" src="index.js"></script>
+ </head>
+ <body>
+ <div class="App">
+ <div class="AlertContainer" id="alert-container"></div>
+ <div class="Container" id="container">
+ <div class="RoomList Hidden" id="left-container">
+ <div class="Header">
+ <h1 class="Title">E/PLACE</h1>
+ <button class="CloseButton" id="close-left">
+ <i class="fa-solid fa-backward"></i>
+ </button>
+ </div>
+ <div class="ListContainer">
+ <div class="ListSearchBar">
+ <div class="InputContainer">
+ <input
+ aria-placeholder="Search a room..."
+ placeholder="Search a room..."
+ class="Input"
+ id="search-input"
+ />
+ <div class="Divider"></div>
+ <button class="RoomButton" id="create-room">
+ <i class="fa-solid fa-square-plus"></i>
+ </button>
+ <button class="RoomButton" id="refresh-rooms">
+ <i class="fa-solid fa-rotate"></i>
+ </button>
+ </div>
+ <div class="FilterContainer">
+ <span class="FilterText">Filters:</span>
+ <div class="FilterHeader">
+ <button class="Filter" id="filter-name">
+ <span>Name</span>
+ <i class="fa-solid fa-sort-down"></i>
+ </button>
+ <button class="Filter" id="filter-owner">
+ <span>Owned by you</span>
+ <i class="fa-solid fa-plus"></i>
+ </button>
+ <button class="Filter" id="filter-public">
+ <span>Public</span>
+ <i class="fa-solid fa-minus"></i>
+ </button>
+ <button class="Filter" id="filter-private">
+ <span>Private</span>
+ <i class="fa-solid fa-minus"></i>
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="RoomsContainer" id="rooms-container"></div>
+ </div>
+ <div class="StudentProfile">
+ <img src="" class="Avatar" id="profile-info-avatar" />
+ <div class="TextContainer">
+ <span class="Login" id="profile-info-login"></span>
+ <span class="Quote" id="profile-info-quote"></span>
+ </div>
+ <button class="ModifyProfileButton" id="profile-update">
+ <i class="fa-solid fa-pencil"></i>
+ </button>
+ </div>
+ </div>
+ <div class="RoomCanvas">
+ <div class="Header">
+ <div class="TextContainer">
+ <h2 class="Title" id="room-name">{{room_name}}</h2>
+ <span class="Description" id="room-description"
+ >{{room_description}}</span
+ >
+ </div>
+ <div class="HeaderDivider"></div>
+ <div class="ButtonContainer">
+ <button class="RoomButton" id="edit-room">
+ <i class="fa-solid fa-pencil"></i>
+ </button>
+ <button class="RoomButton" id="delete-room">
+ <i class="fa-solid fa-trash-can"></i>
+ </button>
+ <button class="ReportButton">Report Abuse</button>
+ </div>
+ </div>
+ <div class="CanvasContainer" id="canvas-container">
+ <canvas class="Canvas" id="canvas"></canvas>
+ <img class="Selector" id="selector" src="/selector.svg" />
+ <div class="Tooltip" id="tooltip">
+ <div class="Header">
+ <div class="PlacedByInfo">
+ <img
+ src="{{student_avatar}}"
+ class="Avatar"
+ id="tooltip-info-avatar"
+ />
+ <div class="Profile">
+ <span class="Login" id="tooltip-info-login"
+ >{{student_login}}</span
+ >
+ <span class="Quote" id="tooltip-info-quote"
+ >{{student_quote}}</span
+ >
+ </div>
+ </div>
+ <div class="TextContainer">
+ <span id="tooltip-date">{{date_placed}}</span>
+ <span id="tooltip-time">{{time_placed}}</span>
+ </div>
+ </div>
+ <div class="ButtonContainer">
+ <button class="ColorPicker" id="color-picker">
+ <i class="fas fa-eye-dropper"></i>
+ </button>
+ <button class="PlaceButton" id="color-place-button">
+ PLACE
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="PositionTooltip" id="position-tooltip">
+ <span>X=0</span>
+ <span>Y=0</span>
+ </div>
+ <div class="ColorWheelContainer" id="color-wheel-container">
+ <div class="ColorWheel" id="color-wheel"></div>
+ </div>
+ </div>
+ <div class="RoomChat" id="right-container">
+ <div class="ChatContainer">
+ <div class="Header">
+ <button class="CloseButton" id="close-right">
+ <i class="fa-solid fa-forward"></i>
+ </button>
+ </div>
+ <div class="ChatMessageContainer" id="chat-message-container"></div>
+ <form
+ class="InputContainer"
+ id="chat-input-form"
+ autocomplete="off"
+ >
+ <input
+ aria-placeholder="Type your message here..."
+ placeholder="Type your message here..."
+ class="Input"
+ id="message-content"
+ name="message-content"
+ autocomplete="off"
+ />
+ <div class="ChatTimeout">
+ <i class="fa-solid fa-stopwatch"></i>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/rushs/eplace/src/pages/index.js b/rushs/eplace/src/pages/index.js
new file mode 100644
index 0000000..59cdb91
--- /dev/null
+++ b/rushs/eplace/src/pages/index.js
@@ -0,0 +1,10 @@
+// FIXME: This is the entry point of the application, write your code here
+
+import { calculateLayout } from "./utils";
+import "./debug";
+import { initSocket } from "../utils/streams";
+
+// Initialize the layout
+calculateLayout();
+
+initSocket();
diff --git a/rushs/eplace/src/pages/styles.less b/rushs/eplace/src/pages/styles.less
new file mode 100644
index 0000000..dd8ae95
--- /dev/null
+++ b/rushs/eplace/src/pages/styles.less
@@ -0,0 +1,905 @@
+@base: rgba(16, 18, 60, 0.95);
+@dark: #05061a;
+@light: #f0f0f0;
+@accent: #ff4603;
+
+.App {
+ background-color: @dark;
+ min-height: 100vh;
+ margin: 0 auto;
+ overflow: hidden;
+ padding: 0rem;
+ position: relative;
+ text-align: center;
+ width: 100vw;
+}
+
+.AlertContainer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: flex;
+ flex-direction: column-reverse;
+ align-items: flex-end;
+ justify-content: flex-end;
+ gap: 1rem;
+ padding: 2rem;
+ z-index: 3;
+ pointer-events: none;
+
+ .Alert {
+ position: relative;
+ background-color: #fff;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.5rem;
+ width: 100%;
+ pointer-events: all;
+
+ box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
+
+ .Icon {
+ font-size: 28px;
+ }
+
+ .AlertBody {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ margin: 0.5rem 2rem 0.5rem 0;
+ text-align: left;
+ font-family: Barlow;
+ color: @dark;
+
+ .AlertTitle {
+ font-size: 1rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ }
+ .AlertContent {
+ font-size: 0.75rem;
+ opacity: 50%;
+ }
+ }
+
+ .AlertClose {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ font-size: 1em;
+ color: @dark;
+ }
+ }
+
+ .AlertSuccess {
+ border-left: 5px solid #49d761;
+
+ .Icon {
+ color: #49d761;
+ }
+ }
+ .AlertWarning {
+ border-left: 5px solid #ff9f1c;
+
+ .Icon {
+ color: #ff9f1c;
+ }
+ }
+ .AlertError {
+ border-left: 5px solid #fd2020;
+
+ .Icon {
+ color: #fd2020;
+ }
+ }
+}
+
+.Container {
+ column-gap: 2em;
+ display: grid;
+ flex-direction: row;
+ grid-auto-flow: row dense;
+ // Fully openned sidebars layout
+ // -> grid-template-columns: 1fr 2.5fr 1fr;
+ // Closed sidebars layout
+ grid-template-columns: 0fr 2.5fr 0fr;
+ height: calc(100vh - 4rem);
+ overflow: hidden;
+ padding: 2rem;
+
+ transition: all 0.5s ease-in-out;
+}
+
+.RoomList {
+ background-color: @base;
+ border-radius: 0.5rem;
+ display: grid;
+ flex-grow: 0;
+ gap: 0.5em 0em;
+ grid-template-rows: 1fr 8.25fr 0.75fr;
+ overflow: hidden;
+ padding: 1.5rem;
+ z-index: 1;
+ // Fully openned sidebar
+ // -> opacity: 1;
+ // Closed sidebar
+ opacity: 0;
+
+ transition: opacity 0.5s ease-in-out;
+
+ .Header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ overflow: hidden;
+
+ .Title {
+ color: @light;
+ font-family: Barlow;
+ font-size: 3rem;
+ font-weight: 800;
+ grid-row-start: 1;
+ overflow: hidden;
+ text-align: start;
+ text-overflow: ellipsis;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+ }
+
+ .ListContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+ margin: 1rem 0rem;
+ overflow: auto;
+ padding: 0rem;
+ }
+
+ .ListSearchBar {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em 0em;
+
+ .FilterContainer {
+ background-color: rgba(black, 0.5);
+ align-items: center;
+ display: flex;
+ font-family: Barlow;
+ font-weight: 800;
+ font-size: 0.75rem;
+ gap: 0em 1em;
+ overflow: hidden;
+ text-align: start;
+ white-space: nowrap;
+ padding: 0.5rem;
+ border-radius: 0.5rem;
+
+ .FilterText {
+ margin-left: 0.5rem;
+ opacity: 0.5;
+ font-size: 0.8rem;
+ }
+
+ .FilterHeader {
+ display: flex;
+ font-family: Barlow;
+ font-weight: 800;
+ overflow: scroll;
+ scrollbar-width: none;
+ text-align: start;
+ gap: 0.5em;
+
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ .Filter {
+ background-color: rgba(@light, 0.25);
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5em;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.65rem;
+ }
+ }
+ }
+ }
+
+ .RoomsContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ grid-row-start: 2;
+ overflow: scroll;
+
+ .Room {
+ background-color: rgba(black, 0.25);
+ border-radius: 0.5rem;
+ display: flex;
+ gap: 1em;
+ align-items: center;
+ padding: 0.5rem;
+
+ &:disabled {
+ border: @accent 0.15rem solid;
+ cursor: not-allowed;
+ }
+
+ .Avatar {
+ height: 4rem;
+ width: 4rem;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .TextContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 0.5em;
+ color: @light;
+ overflow: hidden;
+
+ .Name {
+ font-family: Barlow;
+ font-size: 1rem;
+ font-weight: 800;
+ text-align: start;
+ text-transform: uppercase;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ .RoomPrivacy {
+ margin-right: 0.25rem;
+ }
+ }
+
+ .RoomOwner {
+ font-family: Montserrat;
+ font-size: 0.65rem;
+ font-weight: 700;
+ text-align: start;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ }
+
+ .ListRoomDivider {
+ background-color: rgba(@light, 0.25);
+ height: 0.1rem;
+ margin: 0.5rem 0rem;
+ }
+ }
+
+ .StudentProfile {
+ align-items: center;
+ background-color: @light;
+ border-radius: 5rem;
+ column-gap: 0.5em;
+ display: flex;
+ flex-direction: row;
+ grid-row-start: 3;
+ overflow: hidden;
+ padding: 0.5rem;
+
+ .Avatar {
+ border-radius: 100%;
+ max-height: 3rem;
+ max-width: 3rem;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .TextContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-right: 0.5rem;
+ overflow: hidden;
+ color: black;
+ text-align: start;
+ overflow: hidden;
+
+ .Login {
+ font-family: Barlow;
+ font-size: 1rem;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .Quote {
+ font-family: Montserrat;
+ font-size: 0.7rem;
+ font-weight: 700;
+ opacity: 65%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .ModifyProfileButton {
+ background-color: transparent;
+ color: @dark;
+ padding: 0.25rem;
+ margin-left: auto;
+ margin-right: 0.5rem;
+ border-radius: 100%;
+ }
+ }
+}
+
+.RoomCanvas {
+ display: grid;
+ flex-direction: column;
+ flex-grow: 0;
+ grid-template-rows: 0.25fr 9.75fr;
+ min-width: 50vw;
+ row-gap: 0em;
+ overflow: hidden;
+
+ .Header {
+ background-color: @base;
+ border-radius: 0.5rem;
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ flex-direction: row;
+ gap: 1.5rem;
+ grid-row-start: 1;
+ padding: 0.5rem;
+ z-index: 1;
+
+ .TextContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: end;
+
+ .Title {
+ color: @light;
+ font-family: Barlow;
+ font-size: 2rem;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+
+ .Description {
+ // Start with the description hidden.
+ // It needs to be visible if the room has a description.
+ display: none;
+ position: relative;
+ top: -0.25rem;
+
+ color: @light;
+ font-family: Montserrat;
+ font-size: 0.5rem;
+ font-weight: 700;
+ opacity: 0.5;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .HeaderDivider {
+ background-color: rgba(@light, 0.25);
+ height: 100%;
+ margin: 0.5rem 0rem;
+ width: 0.1rem;
+ }
+
+ .ButtonContainer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+
+ .ReportButton {
+ background-color: @accent;
+ border-radius: 0.5rem;
+ border: 0px solid transparent;
+ color: @light;
+ display: block;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+ height: auto;
+ padding: 0.35rem 0.75rem;
+ text-transform: uppercase;
+ }
+ }
+ }
+
+ .CanvasContainer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ grid-row-start: 2;
+ overflow: hidden;
+ z-index: 0;
+
+ .Canvas {
+ transform: translate(0, 0) scale(2.5);
+ }
+
+ .Selector {
+ position: absolute;
+ inset: 0;
+ width: 1px;
+ height: 1px;
+ margin: auto;
+ pointer-events: none;
+ }
+
+ .Tooltip {
+ // The tooltip is a flexbox
+ // -> display: flex;
+ // Start with the tooltip hidden
+ display: none;
+
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ transform: translate(-50%, -150%);
+ background-color: @base;
+ border-radius: 0.5rem;
+ border: @accent 0.15rem solid;
+ padding: 0.5rem;
+
+ .Header {
+ display: flex;
+ gap: 0.75rem;
+
+ .PlacedByInfo {
+ position: relative;
+ display: inline-block;
+ height: 3rem;
+ width: 3rem;
+
+ .Avatar {
+ border-radius: 100%;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .Profile {
+ visibility: hidden;
+ background-color: @dark;
+ color: @light;
+ text-align: center;
+ padding: 0.5rem;
+ border-radius: 0.5rem;
+
+ bottom: 100%;
+ left: 50%;
+ transform: translate(-50%, 0);
+ position: absolute;
+ z-index: 1;
+
+ &::after {
+ content: " ";
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: @dark transparent transparent transparent;
+ }
+
+ .Login {
+ font-family: Barlow;
+ font-size: 0.8rem;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .Quote {
+ font-family: Montserrat;
+ font-size: 0.7rem;
+ font-weight: 700;
+ opacity: 0.5;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ &:hover .Profile {
+ visibility: visible;
+ }
+ }
+
+ .TextContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-align: left;
+ }
+ }
+
+ .ButtonContainer {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.5rem;
+ width: 100%;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+
+ .ColorPicker {
+ padding: 0.2rem;
+ outline: none;
+ color: white;
+ border: white 0.1rem solid;
+ }
+
+ .PlaceButton {
+ padding: 0.2rem;
+ border: @accent 0.1rem solid;
+ border-radius: 0.5rem;
+ outline: none;
+ font-family: Barlow;
+ font-size: 0.75rem;
+ font-weight: 800;
+
+ &:active {
+ background-color: @accent;
+ color: @light;
+ }
+
+ &:disabled {
+ background-color: @dark;
+ color: @accent;
+ }
+ }
+ }
+ }
+ }
+
+ .PositionTooltip {
+ background-color: rgba(@dark, 0.25);
+ padding: 0.5rem;
+ margin: 0.5rem auto auto 0;
+ border-radius: 0.5rem;
+ font-family: Montserrat;
+ font-weight: 700;
+ z-index: 2;
+ }
+
+ .ColorWheelContainer {
+ // The color wheel is a flexbox
+ // -> display: block;
+ // Start with the color wheel hidden
+ display: none;
+
+ background-color: @base;
+ margin: auto 0 0 auto;
+ padding: 0.5rem;
+ max-height: calc(100% - 12rem);
+ overflow: scroll;
+ border-radius: 1rem;
+ border: @accent 0.15rem solid;
+ z-index: 1;
+
+ min-block-size: -webkit-fill-available;
+
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ transition: all 0.5s ease-in-out;
+
+ .ColorWheel {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ width: 100%;
+ margin: 0.25rem 0;
+ }
+ }
+}
+
+.RoomChat {
+ display: grid;
+ flex-grow: 0;
+ grid-auto-flow: row;
+ grid-template-rows: 1fr 8fr;
+ overflow: hidden;
+ padding: 0rem;
+ z-index: 1;
+ // Fully openned sidebar
+ // -> opacity: 1;
+ // Closed sidebar
+ opacity: 0;
+
+ transition: opacity 0.5s ease-in-out;
+
+ .ChatContainer {
+ background-color: @base;
+ border-radius: 0.5rem;
+ display: grid;
+ gap: 1em;
+ grid-row-start: 2;
+ grid-template-rows: 0.5fr 6.75fr 0.75fr;
+ grid-template-columns: 1fr;
+ overflow: hidden;
+ padding: 0.8rem;
+
+ .Header {
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: center;
+ }
+
+ .ChatMessageContainer {
+ display: flex;
+ flex-direction: column-reverse;
+ overflow: auto;
+ padding: 0rem;
+ row-gap: 1em;
+
+ .ChatMessage {
+ color: @light;
+ background-color: rgba(black, 0.25);
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ padding: 0.5rem;
+
+ .MessageHeader {
+ column-gap: 0.5em;
+ display: flex;
+ padding: 0.5rem;
+ overflow: hidden;
+
+ .Avatar {
+ border-radius: 100%;
+ max-height: 2rem;
+ max-width: 2rem;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ }
+
+ .Login {
+ font-family: Barlow;
+ font-weight: 800;
+ text-align: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .Time {
+ font-family: Barlow;
+ margin-left: auto;
+ text-align: end;
+ }
+ }
+ }
+
+ .ChatUserEvent {
+ background-color: rgba(black, 0.25);
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ padding: 0.25rem;
+ opacity: 0.5;
+ }
+
+ .ChatMessageMentionned {
+ background-color: rgba(@accent, 0.25);
+ }
+
+ .MessageContent {
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 400;
+ overflow-wrap: break-word;
+ padding: 0.5rem;
+ text-align: start;
+ }
+ }
+ }
+}
+
+.Hidden {
+ width: 0;
+ height: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.CloseButton {
+ background-color: @accent;
+ border-radius: 0.5rem;
+ border: 0px solid transparent;
+ color: @light;
+ display: block;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+ max-height: 2rem;
+ padding: 0.5rem 0.75rem;
+ text-transform: uppercase;
+}
+
+.InputContainer {
+ background-color: rgba(black, 0.5);
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ overflow: hidden;
+
+ .Divider {
+ background-color: @light;
+ height: 2rem;
+ width: 0.1rem;
+ opacity: 0.75;
+ }
+}
+
+.RoomButton {
+ background-color: rgba(@light, 0.25);
+ padding: 0.35rem 0.5rem;
+ font-size: 0.75rem;
+}
+
+.ChatTimeout {
+ --fill-percent: 0;
+
+ border-radius: 50%;
+ padding: 0.25rem 0.5rem;
+
+ background: radial-gradient(
+ closest-side,
+ @dark 80%,
+ transparent 0 99.9%,
+ @dark 0
+ ),
+ conic-gradient(@accent calc(var(--fill-percent) * 1%), @dark 0);
+}
+
+.Input {
+ background-color: rgba(black, 0);
+ color: @light;
+ flex-grow: 1;
+ font-family: Montserrat;
+ font-size: 0.75rem;
+ font-weight: 700;
+ opacity: 1;
+ padding: 0.5rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.FormOverlay {
+ position: absolute;
+ inset: 0;
+ background-color: rgba(@dark, 0.95);
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .StylisedForm {
+ background-color: @base;
+ padding: 2rem 1rem;
+ border: 0.25rem solid @accent;
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ align-items: center;
+ justify-content: center;
+ font-family: Barlow;
+ min-width: 65vw;
+ max-width: 90vw;
+ color: @light;
+
+ .FormHeader {
+ display: flex;
+ margin-bottom: 2rem;
+
+ .FormTitle {
+ font-size: 1.5rem;
+ font-weight: 800;
+ text-align: center;
+ }
+ }
+
+ .FormItem {
+ display: grid;
+ grid-template-columns: 1fr 6fr;
+ width: 100%;
+ gap: 1rem;
+ align-items: center;
+ justify-content: space-between;
+
+ .FormLabel {
+ font-size: 1rem;
+ font-weight: 800;
+ text-align: end;
+ }
+
+ .FormInput {
+ color: @light;
+ grid-column: 2;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+ font-family: Montserrat;
+ background-color: @dark;
+ }
+
+ .FormInput[type="checkbox"] {
+ width: auto;
+ margin-right: auto;
+ }
+ }
+
+ .FormButtons {
+ display: flex;
+ margin-top: 1rem;
+ gap: 0.5rem;
+ justify-content: space-around;
+
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+
+ [type="submit"] {
+ background-color: #ff4603;
+ }
+
+ button {
+ background-color: @dark;
+ color: @light;
+ border: 0.1rem solid #ff4603;
+
+ &:hover {
+ border: 0.1rem solid @light;
+ }
+ }
+ }
+ }
+}
diff --git a/rushs/eplace/src/pages/utils.js b/rushs/eplace/src/pages/utils.js
new file mode 100644
index 0000000..f8b1008
--- /dev/null
+++ b/rushs/eplace/src/pages/utils.js
@@ -0,0 +1,95 @@
+/**
+ * Global variables
+ */
+let [leftTimer, rightTimer] = [null, null];
+let [leftSize, rightSize] = [
+ localStorage.getItem("leftSize") ?? 0,
+ localStorage.getItem("rightSize") ?? 0,
+];
+
+const parentContainer = document.getElementById("container");
+
+const leftContainer = document.getElementById("left-container");
+const closeLeftButton = document.getElementById("close-left");
+const rightContainer = document.getElementById("right-container");
+const closeRightButton = document.getElementById("close-right");
+
+leftContainer.classList.toggle("Hidden", !leftSize);
+
+/**
+ * Calculate the layout of the home page
+ */
+export const calculateLayout = () => {
+ const parentContainerSize = `${4.5 - leftSize - rightSize}fr`;
+
+ // left and right are reversed because of the grid layout
+ parentContainer.style.gridTemplateColumns = `${leftSize}fr ${parentContainerSize} ${rightSize}fr`;
+ leftContainer.style.opacity = leftSize;
+ rightContainer.style.opacity = rightSize;
+};
+
+closeLeftButton.addEventListener("click", () => {
+ leftSize = 1 - leftSize;
+ localStorage.setItem("leftSize", leftSize);
+
+ calculateLayout();
+ setTimeout(
+ () => {
+ leftContainer.classList.toggle("Hidden", true);
+ },
+ leftSize ? 0 : 300,
+ );
+});
+
+closeRightButton.addEventListener("click", () => {
+ rightSize = 1 - rightSize;
+ localStorage.setItem("rightSize", rightSize);
+
+ calculateLayout();
+ setTimeout(
+ () => {
+ rightContainer.classList.toggle("Hidden", true);
+ },
+ rightSize ? 0 : 300,
+ );
+});
+
+// If the mouse holds on the left side of the screen, open the left container
+document.addEventListener("mousemove", (e) => {
+ if (e.clientX < 10) {
+ if (!leftTimer) {
+ leftTimer = setTimeout(() => {
+ leftSize = 1;
+ localStorage.setItem("leftSize", leftSize);
+
+ calculateLayout();
+ setTimeout(() => {
+ leftContainer.classList.toggle("Hidden", false);
+ }, 300);
+ }, 200);
+ }
+ } else {
+ clearTimeout(leftTimer);
+ leftTimer = null;
+ }
+});
+
+// If the mouse holds on the right side of the screen, open the right container
+document.addEventListener("mousemove", (e) => {
+ if (e.clientX > window.innerWidth - 10) {
+ if (!rightTimer) {
+ rightTimer = setTimeout(() => {
+ rightSize = 1;
+ localStorage.setItem("rightSize", rightSize);
+
+ calculateLayout();
+ setTimeout(() => {
+ rightContainer.classList.toggle("Hidden", false);
+ }, 300);
+ }, 200);
+ }
+ } else {
+ clearTimeout(rightTimer);
+ rightTimer = null;
+ }
+});
diff --git a/rushs/eplace/src/rooms/canvas/index.js b/rushs/eplace/src/rooms/canvas/index.js
new file mode 100644
index 0000000..440e0d3
--- /dev/null
+++ b/rushs/eplace/src/rooms/canvas/index.js
@@ -0,0 +1,96 @@
+// FIXME: This file should handle the room canvas API
+// Link buttons to their respective functions
+// Functions may include:
+
+import { getStudent } from "../../students";
+import { authedAPIRequest } from "../../utils/auth";
+import { getPlacementData } from "./utils";
+
+// - getCanvas (get the canvas of a room and deserialize it)
+export async function getCanvas(slug) {
+ const config = {
+ method: "get",
+ };
+
+ return authedAPIRequest(`/rooms/${slug}/canvas`, config)
+ .then(async (res) => {
+ if (!res) {
+ return null;
+ }
+
+ const response = await res.json();
+
+ const pixels = response.pixels
+ .split("")
+ .map((c) => c.charCodeAt(0).toString(2).padStart(8, "0"))
+ .join("")
+ .match(/.{5}/g)
+ .map((b) => parseInt(b, 2));
+
+ return pixels;
+ })
+ .catch((error) => {
+ console.log(error);
+ return null;
+ });
+}
+// - subscribeToRoom (subscribe to the stream of a room)
+// - getPixelInfo (get the pixel info of a room)
+export async function getPixelInfo() {
+ const info = getPlacementData();
+
+ const response = await authedAPIRequest(
+ `/rooms/epi-place/canvas/pixels?posX=${info.posX}&posY=${info.posY}`,
+ { method: "get" },
+ );
+
+ if (!response) {
+ return null;
+ } else {
+ return response.json();
+ }
+}
+
+// - placePixel (place a pixel in a room)
+export async function placePixel() {
+ const info = getPlacementData();
+
+ const res = await authedAPIRequest(`/rooms/epi-place/canvas/pixels`, {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ posX: info.posX,
+ posY: info.posY,
+ color: info.color,
+ }),
+ });
+
+ if (!res) {
+ return;
+ }
+
+ const response = await res.json();
+
+ const placeDate = new Date(response.timestamp);
+ const studentInfo = await getStudent(response.placedByUid);
+
+ if (!studentInfo) {
+ return;
+ }
+
+ document.getElementById("tooltip-time").innerHTML =
+ placeDate.toLocaleTimeString();
+ document.getElementById("tooltip-date").innerHTML =
+ placeDate.toLocaleDateString();
+ document
+ .getElementById("tooltip-info-avatar")
+ .setAttribute("src", studentInfo.avatarURL ?? "/default-avatar.png");
+ document.getElementById("tooltip-info-quote").innerHTML = studentInfo.quote;
+ document.getElementById("tooltip-info-login").innerHTML = studentInfo.login;
+}
+document
+ .getElementById("color-place-button")
+ .addEventListener("click", placePixel);
diff --git a/rushs/eplace/src/rooms/canvas/utils.js b/rushs/eplace/src/rooms/canvas/utils.js
new file mode 100644
index 0000000..5737dbb
--- /dev/null
+++ b/rushs/eplace/src/rooms/canvas/utils.js
@@ -0,0 +1,432 @@
+// This file handles the room canvas DOM manipulation
+// Functions includes:
+// - initCanvas (initialize the canvas)
+// - renderCanvasUpdate (render a canvas update)
+// - getPlacementData (get the necessary data to place a pixel)
+// - toggleTooltip (toggle the tooltip and display the pixel's information)
+
+import $ from "jquery";
+import { getPixelInfo } from ".";
+import { getStudent } from "../../students";
+
+const canvasContainer = $("#canvas-container")?.[0];
+const canvas = $("#canvas")?.[0];
+const canvasCtx = canvas.getContext("2d");
+const selector = $("#selector")?.[0];
+
+const positionTooltip = $("#position-tooltip")?.[0];
+const tooltip = $("#tooltip")?.[0];
+const colorPicker = $("#color-picker")?.[0];
+const colorWheelContainer = $("#color-wheel-container")?.[0];
+const colorWheel = $("#color-wheel")?.[0];
+
+/**
+ * Global variables
+ */
+let board, palette, selectedColorIdx;
+let animation;
+
+const zoomSpeed = 1 / 25;
+let zoom = 2.5;
+
+let x, y;
+let cx = 0;
+let cy = 0;
+let target = { x: 0, y: 0 };
+let isDrag = false;
+
+/**
+ * Returns the necessary data to place a pixel
+ * @returns {{color: number, posX: number, posX: number}} the data
+ */
+export const getPlacementData = () => ({
+ color: selectedColorIdx,
+ posX: target.x,
+ posY: target.y,
+});
+
+/**
+ * Toggle the tooltip and display the pixel's information
+ * @param {boolean} state
+ */
+export const toggleTooltip = async (state = false) => {
+ tooltip.style.display = state ? "flex" : "none";
+
+ if (state) {
+ const pixelInfo = await getPixelInfo();
+ const uid = pixelInfo.placedByUid;
+ const placeDate = new Date(pixelInfo.timestamp);
+
+ if (!pixelInfo) {
+ throw new Error(
+ 'An error occured while hitting the "/rooms/epi-place/canvas/pixels" endpoint',
+ );
+ }
+
+ const studentInfo = await getStudent(uid);
+
+ if (!studentInfo) {
+ throw new Error(
+ 'An error occured while hitting the "/students/:id" endpoint',
+ );
+ }
+
+ document.getElementById("tooltip-time").innerHTML =
+ placeDate.toLocaleTimeString();
+ document.getElementById("tooltip-date").innerHTML =
+ placeDate.toLocaleDateString();
+ document
+ .getElementById("tooltip-info-avatar")
+ .setAttribute(
+ "src",
+ studentInfo.avatarURL ?? "/default-avatar.png",
+ );
+ document.getElementById("tooltip-info-quote").innerHTML =
+ studentInfo.quote;
+ document.getElementById("tooltip-info-login").innerHTML =
+ studentInfo.login;
+ // FIXME: You should implement or call a function to get the pixel's information
+ // and display it. Make use of target.x and target.y to get the pixel's position.
+ }
+};
+
+/**
+ * Calculate the target position according to the top left corner of the canvas
+ * @param {*} event
+ * @returns {x: number, y: number} the target position
+ */
+const calculateTarget = (event) => {
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+ const canvasLeft = rect.left + window.pageXOffset;
+ const canvasTop = rect.top + window.pageYOffset;
+
+ return {
+ x: Math.floor(
+ ((event?.pageX ?? window.innerWidth / 2) - canvasLeft) * scaleX,
+ ),
+ y: Math.floor(
+ ((event?.pageY ?? window.innerHeight / 2) - canvasTop) * scaleY,
+ ),
+ };
+};
+
+/**
+ * Update the position tooltip
+ * @param {*} event
+ */
+const positionUpdate = (event) => positionDisplay(calculateTarget(event));
+
+/**
+ * Update the position tooltip
+ * @param {{x: number, y: number}} target
+ */
+const positionDisplay = ({ x, y }) => {
+ positionTooltip.innerText = `X=${x} Y=${y}`;
+ canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
+
+ // We add the canvas.width * zoom to make cx and cy positive
+ let selectorX = cx + canvas.width * zoom;
+ let selectorY = cy + canvas.height * zoom;
+
+ // Make odd canvas align
+ if (canvas.width % 2 !== 0) {
+ selectorX += zoom / 2;
+ selectorY += zoom / 2;
+ }
+
+ // Find the translate
+ selectorX %= zoom;
+ selectorY %= zoom;
+
+ // Center selector on the pixel
+ selectorX -= zoom / 2;
+ selectorY -= zoom / 2;
+
+ selector.style.transform = `translate(${selectorX}px, ${selectorY}px) scale(${zoom})`;
+};
+
+// Toggle the color wheel on click on the color picker
+colorPicker.addEventListener("click", () => {
+ const state = colorWheelContainer.style.display;
+
+ colorWheelContainer.style.display =
+ !state || state === "none" ? "block" : "none";
+});
+
+/**
+ * Transform #RRGGBB to 0xBBGGRRAA
+ * @param {string} hex
+ * @returns {number} the 32 bits color
+ */
+const transformHexTo32Bits = (hex) => {
+ const reverse = hex.substring(1).match(/.{2}/g).reverse().join("");
+
+ return parseInt(`0xFF${reverse}`, 16);
+};
+
+/**
+ * Render the canvas
+ * @param {number[]} pixels
+ * @param {string[]} colors
+ */
+const renderCanvas = (pixels, colors) => {
+ const img = new ImageData(canvas.width, canvas.height);
+ const data = new Uint32Array(img.data.buffer);
+
+ board = pixels;
+ palette = colors;
+ for (let i = 0; i < pixels.length; i++) {
+ data[i] = transformHexTo32Bits(colors[pixels[i]]);
+ }
+
+ canvasCtx.putImageData(img, 0, 0);
+ canvasCtx.imageSmoothingEnabled = false;
+ canvas.style.imageRendering = "pixelated";
+
+ // Remove all the colors from the color wheel
+ while (colorWheel.firstChild) {
+ colorWheel.removeChild(colorWheel.firstChild);
+ }
+
+ // Add the colors to the color wheel
+ for (let i = 0; i < colors.length; i++) {
+ const btn = document.createElement("button");
+
+ colorWheel.appendChild(btn);
+
+ btn.addEventListener("click", () => {
+ selectedColorIdx = i;
+ colorPicker.style.color = colors[i];
+ colorPicker.style.border = `${colors[i]} 0.1rem solid`;
+ });
+
+ btn.style.backgroundColor = colors[i];
+ }
+};
+
+/**
+ * Initialize the canvas
+ * @param {*} roomConfig
+ * @param {number[]} pixels
+ */
+export const initCanvas = (roomConfig, pixels) => {
+ const canvasDimensions = roomConfig.metadata.canvasDimensions;
+
+ canvas.width = canvasDimensions;
+ canvas.height = canvasDimensions;
+
+ positionDisplay({ x: canvasDimensions / 2, y: canvasDimensions / 2 });
+ selectedColorIdx = 0;
+
+ const roomColors = roomConfig.settings.roomColors.split(",");
+
+ colorPicker.style.color = roomColors[0];
+ colorPicker.style.border = `${roomColors[0]} 0.1rem solid`;
+
+ renderCanvas(pixels, roomColors);
+};
+
+/**
+ * Update the canvas
+ * @param {string} color
+ * @param {number} x
+ * @param {number} y
+ */
+export const renderCanvasUpdate = (color, x, y) => {
+ const img = new ImageData(canvas.width, canvas.height);
+ const data = new Uint32Array(img.data.buffer);
+
+ board[y * canvas.width + x] = color;
+ for (let i = 0; i < board.length; i++) {
+ data[i] = transformHexTo32Bits(palette[board[i]]);
+ }
+
+ canvasCtx.putImageData(img, 0, 0);
+};
+
+/**
+ * Reset the canvas values
+ */
+export const resetValues = () => {
+ zoom = 2.5;
+ x = 0;
+ y = 0;
+ cx = 0;
+ cy = 0;
+ isDrag = false;
+
+ positionDisplay({ x, y });
+ colorWheelContainer.style.display = "none";
+ toggleTooltip(false);
+};
+
+// Handle scroll on canvas
+document.addEventListener("wheel", (e) => {
+ // Make sure we're scrolling on the canvas or the body and not the UI
+ if (e.target !== canvas && e.target !== canvasContainer) {
+ return;
+ }
+
+ clearInterval(animation);
+ toggleTooltip(false);
+
+ const delta = Math.sign(e.deltaY) * zoomSpeed;
+ const zoomFactor = 1 + delta;
+ const oldZoom = zoom;
+ const newZoom = Math.max(2.5, Math.min(40, oldZoom * zoomFactor));
+
+ // Get the position of the mouse relative to the canvas
+ const mouseX = e.clientX - window.innerWidth / 2;
+ const mouseY = e.clientY - window.innerHeight / 2;
+
+ // Calculate the new center point based on the mouse position
+ const newCx = mouseX - (mouseX - cx) * (newZoom / oldZoom);
+ const newCy = mouseY - (mouseY - cy) * (newZoom / oldZoom);
+
+ if (newZoom !== oldZoom) {
+ zoom = newZoom;
+ cx = newCx;
+ cy = newCy;
+ positionUpdate();
+ }
+});
+
+// Handle click and drag on canvas
+document.addEventListener("mousedown", (e) => {
+ // Make sure we're clicking on the canvas or the body and not the UI
+ if (e.target !== canvas && e.target !== canvasContainer) {
+ return;
+ }
+
+ e.preventDefault();
+
+ // Ignore if right click
+ if (e.button === 2) {
+ return;
+ }
+
+ clearInterval(animation);
+
+ isDrag = false;
+ x = e.clientX;
+ y = e.clientY;
+
+ document.addEventListener("mousemove", mouseMove);
+});
+
+// Smooth animation
+function easeOutQuart(t, b, c, d) {
+ t /= d;
+ t--;
+ return -c * (t * t * t * t - 1) + b;
+}
+
+// Handle when the user releases the mouse
+document.addEventListener("mouseup", (e) => {
+ document.removeEventListener("mousemove", mouseMove);
+
+ // Make sure we're clicking on the canvas or the body and not the UI
+ if (e.target !== canvas && e.target !== canvasContainer) {
+ return;
+ }
+
+ e.preventDefault();
+
+ // Get the tile position
+ target = calculateTarget(e);
+
+ // Make sure we're clicking on the canvas
+ if (
+ target.x >= 0 &&
+ target.x < canvas.width &&
+ target.y >= 0 &&
+ target.y < canvas.height
+ ) {
+ // We want to differentiate between a click and a drag
+ // If it is a click, we want to move the camera to the clicked tile
+
+ // We wait to see if the position changed
+ // If it did not, we consider it a click
+ if (!isDrag) {
+ const duration = 1000;
+ const startZoom = zoom;
+ const endZoom = Math.max(15, Math.min(40, zoom));
+
+ // Get the position of the click in relation to the center of the screen
+ const clickX = e.clientX - window.innerWidth / 2;
+ const clickY = e.clientY - window.innerHeight / 2;
+ const canvaswidthzoom = canvas.width * startZoom;
+ const canvasheightzoom = canvas.height * startZoom;
+ const startx = (cx + canvaswidthzoom / 2) / startZoom;
+ const starty = (cy + canvasheightzoom / 2) / startZoom;
+ const endx = startx - clickX / startZoom;
+ const endy = starty - clickY / startZoom;
+ const endCx = endx * endZoom - (canvas.width / 2) * endZoom;
+ const endCy = endy * endZoom - (canvas.height / 2) * endZoom;
+ const startCx = cx;
+ const startCy = cy;
+ const startTime = Date.now();
+
+ // If the distance is small enough, we just warp to it
+ if (
+ Math.abs(endCx - startCx) < 10 &&
+ Math.abs(endCy - startCy) < 10
+ ) {
+ cx = endCx;
+ cy = endCy;
+ zoom = endZoom;
+ canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
+ } else {
+ clearInterval(animation);
+
+ animation = setInterval(() => {
+ const elapsed = Date.now() - startTime;
+
+ if (elapsed >= duration) {
+ clearInterval(animation);
+ return;
+ }
+
+ const t = elapsed / duration;
+
+ zoom = easeOutQuart(t, startZoom, endZoom - startZoom, 1);
+ cx = easeOutQuart(t, startCx, endCx - startCx, 1);
+ cy = easeOutQuart(t, startCy, endCy - startCy, 1);
+
+ positionUpdate();
+ }, 10);
+ }
+ }
+
+ // Toggle the tooltip if it is a click
+ toggleTooltip(!isDrag);
+
+ // Update the position of the tooltip
+ positionDisplay(target);
+ }
+});
+
+// Handle mouse move
+const mouseMove = (e) => {
+ e.preventDefault();
+
+ toggleTooltip(false);
+ positionUpdate();
+
+ const dx = e.clientX - x;
+ const dy = e.clientY - y;
+
+ // For a big enough delta, we consider it a drag
+ if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
+ isDrag = true;
+ }
+
+ x = e.clientX;
+ y = e.clientY;
+ cx += dx;
+ cy += dy;
+
+ canvas.style.transform = `translate(${cx}px, ${cy}px) scale(${zoom})`;
+};
diff --git a/rushs/eplace/src/rooms/chat/index.js b/rushs/eplace/src/rooms/chat/index.js
new file mode 100644
index 0000000..493f142
--- /dev/null
+++ b/rushs/eplace/src/rooms/chat/index.js
@@ -0,0 +1,4 @@
+// FIXME: This file should handle the room's chat subscription
+// Functions may include:
+// - subscribeToRoomChat (subscribe to the chat of a room)
+// - sendChatMessage (send a chat message)
diff --git a/rushs/eplace/src/rooms/chat/utils.js b/rushs/eplace/src/rooms/chat/utils.js
new file mode 100644
index 0000000..2d21ab2
--- /dev/null
+++ b/rushs/eplace/src/rooms/chat/utils.js
@@ -0,0 +1,6 @@
+// FIXME: This file should handle the room's chat DOM manipulation
+// Link buttons to their respective functions
+// Handle the chat input form and its submission
+// Functions may include:
+// - displayChatMessage (display a chat message in the DOM)
+// - displayUserEvents (display a user event in the DOM)
diff --git a/rushs/eplace/src/rooms/index.js b/rushs/eplace/src/rooms/index.js
new file mode 100644
index 0000000..dde88e6
--- /dev/null
+++ b/rushs/eplace/src/rooms/index.js
@@ -0,0 +1,50 @@
+// FIXME: This file should handle the rooms API
+// Functions may include:
+
+import { subscribe } from "../utils/streams";
+import { authedAPIRequest } from "../utils/auth";
+
+// - fetchRoomConfig (get the configuration of a room)
+export async function fetchRoomConfig(slug) {
+ const config = {
+ method: "get",
+ };
+
+ return authedAPIRequest(`/rooms/${slug}/config`, config)
+ .then(async (res) => {
+ if (!res) {
+ return null;
+ }
+
+ const response = await res.json();
+
+ document.getElementById("room-name").innerHTML =
+ response.metadata.name;
+ const description = document.getElementById("room-description");
+
+ if (response.metadata.description) {
+ description.innerHTML = response.metadata.description;
+ description.style.display = "inherit";
+ } else {
+ description.style.display = "none";
+ }
+
+ return response;
+ })
+ .catch((error) => {
+ console.log(error);
+ return null;
+ });
+}
+// - joinRoom (join a room by its slug)
+export function joinRoom(slug) {
+ if (!slug) {
+ slug = "epi-place";
+ }
+
+ subscribe(slug);
+}
+// - listRooms (list all the rooms available)
+// - createRoom (create a room)
+// - updateRoom (update a room's configuration)
+// - deleteRoom (delete a room)
diff --git a/rushs/eplace/src/rooms/utils.js b/rushs/eplace/src/rooms/utils.js
new file mode 100644
index 0000000..5e94739
--- /dev/null
+++ b/rushs/eplace/src/rooms/utils.js
@@ -0,0 +1,6 @@
+// FIXME: This file should handle the rooms DOM manipulation
+// Link buttons to their respective functions
+// Functions may include:
+// - showModal (add a form modal to the DOM)
+// - createRoomObject (create a room in the DOM)
+// - displayRoomsList (display the rooms list in the DOM)
diff --git a/rushs/eplace/src/students/index.js b/rushs/eplace/src/students/index.js
new file mode 100644
index 0000000..2aa18a2
--- /dev/null
+++ b/rushs/eplace/src/students/index.js
@@ -0,0 +1,19 @@
+// FIXME: This file should handle the students API
+// Functions may include:
+
+import { authedAPIRequest } from "../utils/auth";
+
+// - getStudent (get a student from the API by its uid or login)
+export async function getStudent(id) {
+ const response = await authedAPIRequest(`/students/${id}`, {
+ method: "get",
+ });
+
+ if (!response) {
+ return null;
+ } else {
+ return response.json();
+ }
+}
+// - getUserUidFromToken (get the user's uid from the token in local storage)
+// - updateStudent (update the student's profile through the API)
diff --git a/rushs/eplace/src/students/utils.js b/rushs/eplace/src/students/utils.js
new file mode 100644
index 0000000..09bb32e
--- /dev/null
+++ b/rushs/eplace/src/students/utils.js
@@ -0,0 +1,5 @@
+// FIXME: This file should handle the students DOM manipulation
+// Link buttons to their respective functions
+// Functions may include:
+// - displayStudentProfile (display the student's profile in the DOM)
+// - showModal (add a form modal to the DOM)
diff --git a/rushs/eplace/src/utils/auth.js b/rushs/eplace/src/utils/auth.js
new file mode 100644
index 0000000..2576282
--- /dev/null
+++ b/rushs/eplace/src/utils/auth.js
@@ -0,0 +1,124 @@
+// FIXME: This file should handle the authentication
+// Exports must include:
+// - authedAPIRequest (make an authenticated request to the API)
+
+/**
+ * This function makes an authenticated request to the API
+ * This function must always hit the /api/* endpoint.
+ * @param {string} endpoint
+ * @param {object} options This object should at least contain the method.
+ * For the other options, you can refer to the fetch documentation as it should be the same.
+ * @returns {Promise<Response>} the response
+ * We want a {Promise<Response>} so we can read the headers as well as the body, rather than
+ * just the body.
+ **/
+export async function authedAPIRequest(endpoint, options) {
+ if (localStorage.getItem("token")) {
+ // essaye de contacter l'endpoint
+ const headers = {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ };
+
+ if (options["headers"]) {
+ options["headers"]["Authorization"] =
+ `Bearer ${localStorage.getItem("token")}`;
+ } else {
+ options["headers"] = headers;
+ }
+
+ const apiURL = `${import.meta.env.VITE_URL}/api${endpoint}`;
+
+ try {
+ const response = await fetch(apiURL, options);
+
+ if (response.status === 200) {
+ return response;
+ } else {
+ console.log("request error");
+ console.log(response);
+ if (
+ response.status === 401 &&
+ (await response.json()).message.match(/Token expired/)
+ ) {
+ localStorage.removeItem("token");
+ if (!(await sendTokenRequest())) {
+ return null;
+ }
+
+ const response = await authedAPIRequest(endpoint, options);
+
+ if (!response) {
+ return null;
+ }
+
+ return response;
+ } else {
+ localStorage.clear();
+ // window.location.replace(import.meta.env.VITE_URL);
+ await sendTokenRequest();
+ return null;
+ }
+ }
+ } catch {
+ console.log("an error occured while fetching");
+ return null;
+ }
+ }
+
+ await sendTokenRequest();
+ return null;
+}
+
+// Functions may include:
+// - sendTokenRequest (get a token or refresh it)
+export async function sendTokenRequest() {
+ if (
+ !localStorage.getItem("token") &&
+ !localStorage.getItem("refresh_token")
+ ) {
+ const authQueryParams = {
+ client_id: import.meta.env.VITE_CLIENT_ID,
+ scope: "epita profile picture",
+ redirect_uri: `${import.meta.env.VITE_URL}/complete/epita/`,
+ response_type: "code",
+ };
+ const url = new URL(`${import.meta.env.VITE_AUTH_URL}/authorize`);
+
+ //`?client_id=${authQueryParams.client_id}&scope=${authQueryParams.scope}&redirect_uri=${authQueryParams.redirect_uri}&response_type=${authQueryParams.response_type}`,
+ url.searchParams.append("client_id", authQueryParams.client_id);
+ url.searchParams.append("scope", authQueryParams.scope);
+ url.searchParams.append("redirect_uri", authQueryParams.redirect_uri);
+ url.searchParams.append("response_type", authQueryParams.response_type);
+
+ window.location.replace(url);
+ return false;
+ } else if (localStorage.getItem("refresh_token")) {
+ const form = new FormData();
+
+ form.append("client_id", import.meta.env.VITE_CLIENT_ID);
+ form.append(
+ "redirect_uri",
+ `${import.meta.env.VITE_URL}/complete/epita/`,
+ );
+ form.append("grant_type", "refresh_token");
+ form.append("refresh_token", localStorage.getItem("refresh_token"));
+ const res = await fetch(`${import.meta.env.VITE_URL}/auth-api/token`, {
+ method: "POST",
+ body: form,
+ });
+
+ if (res.status === 200) {
+ const response = await res.json();
+
+ localStorage.setItem("token", response.id_token);
+ localStorage.setItem("refresh_token", response.refresh_token);
+ return true;
+ } else {
+ localStorage.clear();
+ // window.location.replace(import.meta.env.VITE_URL);
+ // HERE ptetre return direct senTokenRequest
+ await sendTokenRequest();
+ return false;
+ }
+ }
+}
diff --git a/rushs/eplace/src/utils/notify.js b/rushs/eplace/src/utils/notify.js
new file mode 100644
index 0000000..b6ed7dc
--- /dev/null
+++ b/rushs/eplace/src/utils/notify.js
@@ -0,0 +1,57 @@
+import $ from "jquery";
+import alertHtml from "../components/notifications/index.html";
+
+const alertContainer = $("#alert-container")?.[0];
+
+const iconMap = {
+ info: "fa-info-circle",
+ success: "fa-thumbs-up",
+ warning: "fa-exclamation-triangle",
+ error: "ffa fa-exclamation-circle",
+};
+
+/**
+ * Create an alert
+ * @param {string} title
+ * @param {string} message
+ * @param {string} type - success, warning, error
+ */
+export const createAlert = (title, message, type) => {
+ $.ajax({
+ url: alertHtml,
+ success: (data) => {
+ const [alert] = $(data);
+
+ // Return if the alert cannot be created, usefull when a redirect is made
+ if (!alertContainer || !alert || !alert.classList) {
+ return;
+ }
+
+ // Add the type class to the alert
+ alert.classList.add(
+ `Alert${type.charAt(0).toUpperCase() + type.slice(1)}`,
+ );
+
+ // Replace values in innerHTML
+ alert.innerHTML = alert.innerHTML
+ .replace(/{{title}}/g, title)
+ .replace(/{{content}}/g, message)
+ .replace(/{{icon_classes}}/g, iconMap[type]);
+
+ // Get the close button
+ const closeBtn = alert.getElementsByClassName("AlertClose")?.[0];
+
+ closeBtn?.addEventListener("click", () => {
+ alert.remove();
+ });
+
+ // Append the alert to the container
+ alertContainer.append(alert);
+
+ // Remove the alert after 5 seconds
+ setTimeout(() => {
+ alert.remove();
+ }, 5000);
+ },
+ });
+};
diff --git a/rushs/eplace/src/utils/rateLimits.js b/rushs/eplace/src/utils/rateLimits.js
new file mode 100644
index 0000000..d7b11a6
--- /dev/null
+++ b/rushs/eplace/src/utils/rateLimits.js
@@ -0,0 +1,3 @@
+// FIXME: This file should handle the rate limits
+// Functions may include:
+// - displayTimer (util function to display the timer for the rate limit)
diff --git a/rushs/eplace/src/utils/streams.js b/rushs/eplace/src/utils/streams.js
new file mode 100644
index 0000000..f714527
--- /dev/null
+++ b/rushs/eplace/src/utils/streams.js
@@ -0,0 +1,108 @@
+// FIXME: This file should handle the sockets and the subscriptions
+import { io } from "socket.io-client";
+import { v4 as uuidv4 } from "uuid";
+
+import { createAlert } from "./notify";
+import { fetchRoomConfig, joinRoom } from "../rooms";
+import { getCanvas } from "../rooms/canvas";
+import { initCanvas, renderCanvasUpdate } from "../rooms/canvas/utils";
+import { sendTokenRequest } from "../utils/auth";
+
+let subscribed = false;
+
+export let socket = undefined;
+
+const updateBuffer = [];
+
+// Exports must include
+// - initSocket (initialize the connection to the socket server)
+export async function initSocket() {
+ if (!localStorage.getItem("token")) {
+ if (!(await sendTokenRequest())) {
+ createAlert("ForgeID", "Authentication failed", "error");
+ return;
+ }
+
+ createAlert("ForgeID", "Authentication successful", "success");
+ }
+
+ if (socket) {
+ return socket;
+ }
+
+ socket = io(import.meta.env.VITE_URL, {
+ extraHeaders: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ },
+ });
+ socket.on("connect", () => {
+ joinRoom();
+ });
+
+ socket.on("message", async (msg) => {
+ if (msg.result && msg.result.type === "started") {
+ const roomConfig = await fetchRoomConfig("epi-place");
+
+ if (!roomConfig) {
+ return;
+ }
+
+ const pixels = await getCanvas("epi-place");
+
+ if (!pixels) {
+ return;
+ }
+
+ initCanvas(roomConfig, pixels);
+ // debuffer pixel updates
+ catchUpOnUpdates();
+ subscribed = true;
+ }
+ });
+
+ socket.on("pixel-update", (update) => {
+ if (!subscribed) {
+ updateBuffer.push({
+ color: update.result.data.json.color,
+ posX: update.result.data.json.posX,
+ posY: update.result.data.json.posY,
+ });
+ } else {
+ renderCanvasUpdate(
+ update.result.data.json.color,
+ update.result.data.json.posX,
+ update.result.data.json.posY,
+ );
+ }
+ });
+ return socket;
+}
+// - socket (variable resulting of initSocket function)
+
+// Functions may include:
+// - subscribe (subscribe to a room's stream or chat)
+export function subscribe(slug) {
+ const subscription = {
+ id: uuidv4(),
+ method: "subscription",
+ params: {
+ path: "rooms.canvas.getStream",
+ input: {
+ json: {
+ roomSlug: slug,
+ },
+ },
+ },
+ };
+
+ socket.send(subscription);
+}
+function catchUpOnUpdates() {
+ while (updateBuffer.length > 0) {
+ const update = updateBuffer.pop();
+
+ renderCanvasUpdate(update.color, update.posX, update.posY);
+ }
+}
+// - unsubscribe (unsubscribe from a room's stream or chat)
+// - sendMessage (send a message to a room's chat)
diff --git a/rushs/eplace/vite.config.js b/rushs/eplace/vite.config.js
new file mode 100644
index 0000000..bb8b7a2
--- /dev/null
+++ b/rushs/eplace/vite.config.js
@@ -0,0 +1,41 @@
+import { resolve } from "path";
+import { defineConfig, loadEnv } from "vite";
+import dns from "dns";
+
+dns.setDefaultResultOrder("verbatim");
+const root = resolve(__dirname, "src/pages/");
+
+export default ({ mode }) => {
+ process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
+
+ return defineConfig({
+ root,
+ server: {
+ host: process.env.VITE_HOST,
+ port: process.env.VITE_PORT,
+ proxy: {
+ // $VITE_URL/api* -> $VITE_API_URL/api*
+ "/api": {
+ target: process.env.VITE_API_URL,
+ changeOrigin: true,
+ },
+ // $VITE_URL/socket.io* -> $VITE_API_URL/socket.io*
+ "/socket.io": {
+ target: process.env.VITE_API_URL,
+ changeOrigin: true,
+ ws: true,
+ },
+ // $VITE_URL/auth-api* -> $VITE_AUTH_URL*
+ "/auth-api": {
+ target: process.env.VITE_AUTH_URL,
+ changeOrigin: true,
+ secure: false,
+ rewrite: (path) => path.replace(/^\/auth-api/, ""),
+ },
+ },
+ },
+
+ publicDir: resolve(__dirname, "public"),
+ assetsInclude: ["src/components/**/*.html"],
+ });
+};
diff --git a/rushs/eplace/yarn.lock b/rushs/eplace/yarn.lock
new file mode 100644
index 0000000..8b7336f
--- /dev/null
+++ b/rushs/eplace/yarn.lock
@@ -0,0 +1,1957 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@esbuild/android-arm64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.15.tgz#893ad71f3920ccb919e1757c387756a9bca2ef42"
+ integrity sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==
+
+"@esbuild/android-arm@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.15.tgz#143e0d4e4c08c786ea410b9a7739779a9a1315d8"
+ integrity sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==
+
+"@esbuild/android-x64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.15.tgz#d2d12a7676b2589864281b2274355200916540bc"
+ integrity sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==
+
+"@esbuild/darwin-arm64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.15.tgz#2e88e79f1d327a2a7d9d06397e5232eb0a473d61"
+ integrity sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==
+
+"@esbuild/darwin-x64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.15.tgz#9384e64c0be91388c57be6d3a5eaf1c32a99c91d"
+ integrity sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==
+
+"@esbuild/freebsd-arm64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.15.tgz#2ad5a35bc52ebd9ca6b845dbc59ba39647a93c1a"
+ integrity sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==
+
+"@esbuild/freebsd-x64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.15.tgz#b513a48446f96c75fda5bef470e64d342d4379cd"
+ integrity sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==
+
+"@esbuild/linux-arm64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.15.tgz#9697b168175bfd41fa9cc4a72dd0d48f24715f31"
+ integrity sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==
+
+"@esbuild/linux-arm@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.15.tgz#5b22062c54f48cd92fab9ffd993732a52db70cd3"
+ integrity sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==
+
+"@esbuild/linux-ia32@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.15.tgz#eb28a13f9b60b5189fcc9e98e1024f6b657ba54c"
+ integrity sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==
+
+"@esbuild/linux-loong64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.15.tgz#32454bdfe144cf74b77895a8ad21a15cb81cfbe5"
+ integrity sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==
+
+"@esbuild/linux-mips64el@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.15.tgz#af12bde0d775a318fad90eb13a0455229a63987c"
+ integrity sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==
+
+"@esbuild/linux-ppc64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.15.tgz#34c5ed145b2dfc493d3e652abac8bd3baa3865a5"
+ integrity sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==
+
+"@esbuild/linux-riscv64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.15.tgz#87bd515e837f2eb004b45f9e6a94dc5b93f22b92"
+ integrity sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==
+
+"@esbuild/linux-s390x@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.15.tgz#20bf7947197f199ddac2ec412029a414ceae3aa3"
+ integrity sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==
+
+"@esbuild/linux-x64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.15.tgz"
+ integrity sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==
+
+"@esbuild/netbsd-x64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.15.tgz#8da299b3ac6875836ca8cdc1925826498069ac65"
+ integrity sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==
+
+"@esbuild/openbsd-x64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.15.tgz#04a1ec3d4e919714dba68dcf09eeb1228ad0d20c"
+ integrity sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==
+
+"@esbuild/sunos-x64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.15.tgz#6694ebe4e16e5cd7dab6505ff7c28f9c1c695ce5"
+ integrity sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==
+
+"@esbuild/win32-arm64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.15.tgz#1f95b2564193c8d1fee8f8129a0609728171d500"
+ integrity sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==
+
+"@esbuild/win32-ia32@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.15.tgz#c362b88b3df21916ed7bcf75c6d09c6bf3ae354a"
+ integrity sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==
+
+"@esbuild/win32-x64@0.17.15":
+ version "0.17.15"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz#c2e737f3a201ebff8e2ac2b8e9f246b397ad19b8"
+ integrity sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==
+
+"@eslint-community/eslint-utils@^4.2.0":
+ version "4.4.0"
+ resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz"
+ integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
+ dependencies:
+ eslint-visitor-keys "^3.3.0"
+
+"@eslint-community/eslint-utils@^4.4.0":
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz#e4c58fdcf0696e7a5f19c30201ed43123ab15abc"
+ integrity sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==
+ dependencies:
+ eslint-visitor-keys "^3.4.3"
+
+"@eslint-community/regexpp@^4.12.1":
+ version "4.12.1"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
+ integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
+
+"@eslint/config-array@^0.20.0":
+ version "0.20.0"
+ resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.20.0.tgz#7a1232e82376712d3340012a2f561a2764d1988f"
+ integrity sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==
+ dependencies:
+ "@eslint/object-schema" "^2.1.6"
+ debug "^4.3.1"
+ minimatch "^3.1.2"
+
+"@eslint/config-helpers@^0.2.1":
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.1.tgz#26042c028d1beee5ce2235a7929b91c52651646d"
+ integrity sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==
+
+"@eslint/core@^0.13.0":
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.13.0.tgz#bf02f209846d3bf996f9e8009db62df2739b458c"
+ integrity sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==
+ dependencies:
+ "@types/json-schema" "^7.0.15"
+
+"@eslint/eslintrc@^3.3.1":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964"
+ integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.3.2"
+ espree "^10.0.1"
+ globals "^14.0.0"
+ ignore "^5.2.0"
+ import-fresh "^3.2.1"
+ js-yaml "^4.1.0"
+ minimatch "^3.1.2"
+ strip-json-comments "^3.1.1"
+
+"@eslint/js@9.25.1":
+ version "9.25.1"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117"
+ integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==
+
+"@eslint/object-schema@^2.1.6":
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
+ integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
+
+"@eslint/plugin-kit@^0.2.8":
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz#47488d8f8171b5d4613e833313f3ce708e3525f8"
+ integrity sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==
+ dependencies:
+ "@eslint/core" "^0.13.0"
+ levn "^0.4.1"
+
+"@humanfs/core@^0.19.1":
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
+ integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
+
+"@humanfs/node@^0.16.6":
+ version "0.16.6"
+ resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e"
+ integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==
+ dependencies:
+ "@humanfs/core" "^0.19.1"
+ "@humanwhocodes/retry" "^0.3.0"
+
+"@humanwhocodes/module-importer@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz"
+ integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
+
+"@humanwhocodes/retry@^0.3.0":
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
+ integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
+
+"@humanwhocodes/retry@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.2.tgz#1860473de7dfa1546767448f333db80cb0ff2161"
+ integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@pkgr/core@^0.2.3":
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c"
+ integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==
+
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz"
+ integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+
+"@types/estree@^1.0.6":
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
+ integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
+
+"@types/json-schema@^7.0.15":
+ version "7.0.15"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
+ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+
+"@types/json5@^0.0.29":
+ version "0.0.29"
+ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
+ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+
+"@typescript-eslint/scope-manager@8.31.0":
+ version "8.31.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz#48c7f7d729ea038e36cae0ff511e48c2412fb11c"
+ integrity sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==
+ dependencies:
+ "@typescript-eslint/types" "8.31.0"
+ "@typescript-eslint/visitor-keys" "8.31.0"
+
+"@typescript-eslint/types@8.31.0":
+ version "8.31.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.0.tgz#c48e20ec47a43b72747714f49ea9f7b38a4fa6c1"
+ integrity sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==
+
+"@typescript-eslint/typescript-estree@8.31.0":
+ version "8.31.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz#9c7f84eff6ad23d63cf086c6e93af571cd561270"
+ integrity sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==
+ dependencies:
+ "@typescript-eslint/types" "8.31.0"
+ "@typescript-eslint/visitor-keys" "8.31.0"
+ debug "^4.3.4"
+ fast-glob "^3.3.2"
+ is-glob "^4.0.3"
+ minimatch "^9.0.4"
+ semver "^7.6.0"
+ ts-api-utils "^2.0.1"
+
+"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0":
+ version "8.31.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.0.tgz#6fb52471a29fdd16fc253d568c5ad4b048f78ba4"
+ integrity sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.4.0"
+ "@typescript-eslint/scope-manager" "8.31.0"
+ "@typescript-eslint/types" "8.31.0"
+ "@typescript-eslint/typescript-estree" "8.31.0"
+
+"@typescript-eslint/visitor-keys@8.31.0":
+ version "8.31.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz#9a1a97ed16c60d4d1e7399b41c11a6d94ebc1ce5"
+ integrity sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==
+ dependencies:
+ "@typescript-eslint/types" "8.31.0"
+ eslint-visitor-keys "^4.2.0"
+
+acorn-jsx@^5.3.2:
+ version "5.3.2"
+ resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
+ integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn@^8.14.0:
+ version "8.14.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
+ integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
+
+ajv@^6.12.4:
+ version "6.12.6"
+ resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+array-buffer-byte-length@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz"
+ integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==
+ dependencies:
+ call-bind "^1.0.2"
+ is-array-buffer "^3.0.1"
+
+array-includes@^3.1.6:
+ version "3.1.6"
+ resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz"
+ integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+ get-intrinsic "^1.1.3"
+ is-string "^1.0.7"
+
+array.prototype.flat@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz"
+ integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+ es-shim-unscopables "^1.0.0"
+
+array.prototype.flatmap@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz"
+ integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+ es-shim-unscopables "^1.0.0"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+available-typed-arrays@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz"
+ integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
+
+axios@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz"
+ integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
+ dependencies:
+ follow-redirects "^1.15.0"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+ integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+ dependencies:
+ balanced-match "^1.0.0"
+
+braces@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+builtins@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz"
+ integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==
+ dependencies:
+ semver "^7.0.0"
+
+call-bind@^1.0.0, call-bind@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
+ integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+ dependencies:
+ function-bind "^1.1.1"
+ get-intrinsic "^1.0.2"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+chalk@^4.0.0:
+ version "4.1.2"
+ resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+copy-anything@^2.0.1:
+ version "2.0.6"
+ resolved "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz"
+ integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==
+ dependencies:
+ is-what "^3.14.1"
+
+cross-spawn@^7.0.6:
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+data-uri-to-buffer@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz"
+ integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
+
+debug@^3.2.6, debug@^3.2.7:
+ version "3.2.7"
+ resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
+ integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.3.1, debug@^4.3.4:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
+ integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+ dependencies:
+ ms "^2.1.3"
+
+debug@^4.3.2, debug@~4.3.1, debug@~4.3.2:
+ version "4.3.4"
+ resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
+deep-is@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz"
+ integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+
+define-properties@^1.1.3, define-properties@^1.1.4:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz"
+ integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==
+ dependencies:
+ has-property-descriptors "^1.0.0"
+ object-keys "^1.1.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+doctrine@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz"
+ integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
+ dependencies:
+ esutils "^2.0.2"
+
+engine.io-client@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz"
+ integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+ engine.io-parser "~5.0.3"
+ ws "~8.11.0"
+ xmlhttprequest-ssl "~2.0.0"
+
+engine.io-parser@~5.0.3:
+ version "5.0.6"
+ resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz"
+ integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==
+
+errno@^0.1.1:
+ version "0.1.8"
+ resolved "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz"
+ integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
+ dependencies:
+ prr "~1.0.1"
+
+es-abstract@^1.19.0, es-abstract@^1.20.4:
+ version "1.21.2"
+ resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz"
+ integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==
+ dependencies:
+ array-buffer-byte-length "^1.0.0"
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ es-set-tostringtag "^2.0.1"
+ es-to-primitive "^1.2.1"
+ function.prototype.name "^1.1.5"
+ get-intrinsic "^1.2.0"
+ get-symbol-description "^1.0.0"
+ globalthis "^1.0.3"
+ gopd "^1.0.1"
+ has "^1.0.3"
+ has-property-descriptors "^1.0.0"
+ has-proto "^1.0.1"
+ has-symbols "^1.0.3"
+ internal-slot "^1.0.5"
+ is-array-buffer "^3.0.2"
+ is-callable "^1.2.7"
+ is-negative-zero "^2.0.2"
+ is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.2"
+ is-string "^1.0.7"
+ is-typed-array "^1.1.10"
+ is-weakref "^1.0.2"
+ object-inspect "^1.12.3"
+ object-keys "^1.1.1"
+ object.assign "^4.1.4"
+ regexp.prototype.flags "^1.4.3"
+ safe-regex-test "^1.0.0"
+ string.prototype.trim "^1.2.7"
+ string.prototype.trimend "^1.0.6"
+ string.prototype.trimstart "^1.0.6"
+ typed-array-length "^1.0.4"
+ unbox-primitive "^1.0.2"
+ which-typed-array "^1.1.9"
+
+es-set-tostringtag@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz"
+ integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==
+ dependencies:
+ get-intrinsic "^1.1.3"
+ has "^1.0.3"
+ has-tostringtag "^1.0.0"
+
+es-shim-unscopables@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz"
+ integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==
+ dependencies:
+ has "^1.0.3"
+
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+esbuild@^0.17.5:
+ version "0.17.15"
+ resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz"
+ integrity sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==
+ optionalDependencies:
+ "@esbuild/android-arm" "0.17.15"
+ "@esbuild/android-arm64" "0.17.15"
+ "@esbuild/android-x64" "0.17.15"
+ "@esbuild/darwin-arm64" "0.17.15"
+ "@esbuild/darwin-x64" "0.17.15"
+ "@esbuild/freebsd-arm64" "0.17.15"
+ "@esbuild/freebsd-x64" "0.17.15"
+ "@esbuild/linux-arm" "0.17.15"
+ "@esbuild/linux-arm64" "0.17.15"
+ "@esbuild/linux-ia32" "0.17.15"
+ "@esbuild/linux-loong64" "0.17.15"
+ "@esbuild/linux-mips64el" "0.17.15"
+ "@esbuild/linux-ppc64" "0.17.15"
+ "@esbuild/linux-riscv64" "0.17.15"
+ "@esbuild/linux-s390x" "0.17.15"
+ "@esbuild/linux-x64" "0.17.15"
+ "@esbuild/netbsd-x64" "0.17.15"
+ "@esbuild/openbsd-x64" "0.17.15"
+ "@esbuild/sunos-x64" "0.17.15"
+ "@esbuild/win32-arm64" "0.17.15"
+ "@esbuild/win32-ia32" "0.17.15"
+ "@esbuild/win32-x64" "0.17.15"
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+eslint-config-prettier@^8.8.0:
+ version "8.8.0"
+ resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz"
+ integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
+
+eslint-config-standard@^17.0.0:
+ version "17.0.0"
+ resolved "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz"
+ integrity sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==
+
+eslint-import-resolver-node@^0.3.7:
+ version "0.3.7"
+ resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz"
+ integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==
+ dependencies:
+ debug "^3.2.7"
+ is-core-module "^2.11.0"
+ resolve "^1.22.1"
+
+eslint-module-utils@^2.7.4:
+ version "2.7.4"
+ resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz"
+ integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==
+ dependencies:
+ debug "^3.2.7"
+
+eslint-plugin-es@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz"
+ integrity sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==
+ dependencies:
+ eslint-utils "^2.0.0"
+ regexpp "^3.0.0"
+
+eslint-plugin-eslint-comments@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz#9e1cd7b4413526abb313933071d7aba05ca12ffa"
+ integrity sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==
+ dependencies:
+ escape-string-regexp "^1.0.5"
+ ignore "^5.0.5"
+
+eslint-plugin-import@^2.25.2:
+ version "2.27.5"
+ resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz"
+ integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==
+ dependencies:
+ array-includes "^3.1.6"
+ array.prototype.flat "^1.3.1"
+ array.prototype.flatmap "^1.3.1"
+ debug "^3.2.7"
+ doctrine "^2.1.0"
+ eslint-import-resolver-node "^0.3.7"
+ eslint-module-utils "^2.7.4"
+ has "^1.0.3"
+ is-core-module "^2.11.0"
+ is-glob "^4.0.3"
+ minimatch "^3.1.2"
+ object.values "^1.1.6"
+ resolve "^1.22.1"
+ semver "^6.3.0"
+ tsconfig-paths "^3.14.1"
+
+eslint-plugin-jest@^28.11.0:
+ version "28.11.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz#2641ecb4411941bbddb3d7cf8a8ff1163fbb510e"
+ integrity sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==
+ dependencies:
+ "@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0"
+
+eslint-plugin-n@^15.0.0:
+ version "15.7.0"
+ resolved "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz"
+ integrity sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==
+ dependencies:
+ builtins "^5.0.1"
+ eslint-plugin-es "^4.1.0"
+ eslint-utils "^3.0.0"
+ ignore "^5.1.1"
+ is-core-module "^2.11.0"
+ minimatch "^3.1.2"
+ resolve "^1.22.1"
+ semver "^7.3.8"
+
+eslint-plugin-prettier@^5.2.6:
+ version "5.2.6"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz#be39e3bb23bb3eeb7e7df0927cdb46e4d7945096"
+ integrity sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==
+ dependencies:
+ prettier-linter-helpers "^1.0.0"
+ synckit "^0.11.0"
+
+eslint-plugin-promise@^6.0.0:
+ version "6.1.1"
+ resolved "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz"
+ integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==
+
+eslint-scope@^8.3.0:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.3.0.tgz#10cd3a918ffdd722f5f3f7b5b83db9b23c87340d"
+ integrity sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^5.2.0"
+
+eslint-utils@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz"
+ integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
+ dependencies:
+ eslint-visitor-keys "^1.1.0"
+
+eslint-utils@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz"
+ integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
+ dependencies:
+ eslint-visitor-keys "^2.0.0"
+
+eslint-visitor-keys@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz"
+ integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
+
+eslint-visitor-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz"
+ integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
+
+eslint-visitor-keys@^3.3.0:
+ version "3.4.0"
+ resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz"
+ integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
+
+eslint-visitor-keys@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
+ integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
+
+eslint-visitor-keys@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
+ integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
+
+eslint@^9.25.1:
+ version "9.25.1"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.25.1.tgz#8a7cf8dd0e6acb858f86029720adb1785ee57580"
+ integrity sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.2.0"
+ "@eslint-community/regexpp" "^4.12.1"
+ "@eslint/config-array" "^0.20.0"
+ "@eslint/config-helpers" "^0.2.1"
+ "@eslint/core" "^0.13.0"
+ "@eslint/eslintrc" "^3.3.1"
+ "@eslint/js" "9.25.1"
+ "@eslint/plugin-kit" "^0.2.8"
+ "@humanfs/node" "^0.16.6"
+ "@humanwhocodes/module-importer" "^1.0.1"
+ "@humanwhocodes/retry" "^0.4.2"
+ "@types/estree" "^1.0.6"
+ "@types/json-schema" "^7.0.15"
+ ajv "^6.12.4"
+ chalk "^4.0.0"
+ cross-spawn "^7.0.6"
+ debug "^4.3.2"
+ escape-string-regexp "^4.0.0"
+ eslint-scope "^8.3.0"
+ eslint-visitor-keys "^4.2.0"
+ espree "^10.3.0"
+ esquery "^1.5.0"
+ esutils "^2.0.2"
+ fast-deep-equal "^3.1.3"
+ file-entry-cache "^8.0.0"
+ find-up "^5.0.0"
+ glob-parent "^6.0.2"
+ ignore "^5.2.0"
+ imurmurhash "^0.1.4"
+ is-glob "^4.0.0"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ lodash.merge "^4.6.2"
+ minimatch "^3.1.2"
+ natural-compare "^1.4.0"
+ optionator "^0.9.3"
+
+espree@^10.0.1, espree@^10.3.0:
+ version "10.3.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a"
+ integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==
+ dependencies:
+ acorn "^8.14.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^4.2.0"
+
+esquery@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
+ integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
+ dependencies:
+ estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz"
+ integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+ dependencies:
+ estraverse "^5.2.0"
+
+estraverse@^5.1.0, estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-diff@^1.1.2:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
+ integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
+
+fast-glob@^3.3.2:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
+ integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.8"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
+ integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+
+fastq@^1.6.0:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
+ integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
+ dependencies:
+ reusify "^1.0.4"
+
+fetch-blob@^3.1.2, fetch-blob@^3.1.4:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz"
+ integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
+ dependencies:
+ node-domexception "^1.0.0"
+ web-streams-polyfill "^3.0.3"
+
+file-entry-cache@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
+ integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
+ dependencies:
+ flat-cache "^4.0.0"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
+flat-cache@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
+ integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
+ dependencies:
+ flatted "^3.2.9"
+ keyv "^4.5.4"
+
+flatted@^3.2.9:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
+ integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
+
+follow-redirects@^1.15.0:
+ version "1.15.2"
+ resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz"
+ integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
+for-each@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz"
+ integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+ dependencies:
+ is-callable "^1.1.3"
+
+form-data@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz"
+ integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
+formdata-polyfill@^4.0.10:
+ version "4.0.10"
+ resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz"
+ integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
+ dependencies:
+ fetch-blob "^3.1.2"
+
+fsevents@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+function.prototype.name@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz"
+ integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.0"
+ functions-have-names "^1.2.2"
+
+functions-have-names@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz"
+ integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz"
+ integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.3"
+
+get-symbol-description@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz"
+ integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.1"
+
+glob-parent@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz"
+ integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+ dependencies:
+ is-glob "^4.0.3"
+
+globals@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+ integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
+globalthis@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz"
+ integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==
+ dependencies:
+ define-properties "^1.1.3"
+
+gopd@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz"
+ integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+ dependencies:
+ get-intrinsic "^1.1.3"
+
+graceful-fs@^4.1.2:
+ version "4.2.11"
+ resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+has-bigints@^1.0.1, has-bigints@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz"
+ integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-property-descriptors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz"
+ integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
+ dependencies:
+ get-intrinsic "^1.1.1"
+
+has-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz"
+ integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
+
+has-symbols@^1.0.2, has-symbols@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz"
+ integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+has-tostringtag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz"
+ integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
+ dependencies:
+ has-symbols "^1.0.2"
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+iconv-lite@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+ignore@^5.0.5:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
+ integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
+
+ignore@^5.1.1, ignore@^5.2.0:
+ version "5.2.4"
+ resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz"
+ integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+
+image-size@~0.5.0:
+ version "0.5.5"
+ resolved "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz"
+ integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
+
+import-fresh@^3.2.1:
+ version "3.3.0"
+ resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"
+ integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+ dependencies:
+ parent-module "^1.0.0"
+ resolve-from "^4.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
+ integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+internal-slot@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz"
+ integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==
+ dependencies:
+ get-intrinsic "^1.2.0"
+ has "^1.0.3"
+ side-channel "^1.0.4"
+
+is-array-buffer@^3.0.1, is-array-buffer@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz"
+ integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.2.0"
+ is-typed-array "^1.1.10"
+
+is-bigint@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz"
+ integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
+ dependencies:
+ has-bigints "^1.0.1"
+
+is-boolean-object@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz"
+ integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz"
+ integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
+
+is-core-module@^2.11.0:
+ version "2.11.0"
+ resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz"
+ integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+ dependencies:
+ has "^1.0.3"
+
+is-date-object@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz"
+ integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-negative-zero@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz"
+ integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
+
+is-number-object@^1.0.4:
+ version "1.0.7"
+ resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz"
+ integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-regex@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz"
+ integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-shared-array-buffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz"
+ integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
+ dependencies:
+ call-bind "^1.0.2"
+
+is-string@^1.0.5, is-string@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz"
+ integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-symbol@^1.0.2, is-symbol@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz"
+ integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
+ dependencies:
+ has-symbols "^1.0.2"
+
+is-typed-array@^1.1.10, is-typed-array@^1.1.9:
+ version "1.1.10"
+ resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz"
+ integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-tostringtag "^1.0.0"
+
+is-weakref@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz"
+ integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+ dependencies:
+ call-bind "^1.0.2"
+
+is-what@^3.14.1:
+ version "3.14.1"
+ resolved "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz"
+ integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+jquery@^3.6.4:
+ version "3.6.4"
+ resolved "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz"
+ integrity sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==
+
+js-yaml@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ dependencies:
+ argparse "^2.0.1"
+
+json-buffer@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+ integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
+ integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
+
+json5@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz"
+ integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
+ dependencies:
+ minimist "^1.2.0"
+
+jwt-decode@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz"
+ integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==
+
+keyv@^4.5.4:
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
+ integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
+ dependencies:
+ json-buffer "3.0.1"
+
+less@^4.1.3:
+ version "4.1.3"
+ resolved "https://registry.npmjs.org/less/-/less-4.1.3.tgz"
+ integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==
+ dependencies:
+ copy-anything "^2.0.1"
+ parse-node-version "^1.0.1"
+ tslib "^2.3.0"
+ optionalDependencies:
+ errno "^0.1.1"
+ graceful-fs "^4.1.2"
+ image-size "~0.5.0"
+ make-dir "^2.1.0"
+ mime "^1.4.1"
+ needle "^3.1.0"
+ source-map "~0.6.0"
+
+levn@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz"
+ integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+ dependencies:
+ prelude-ls "^1.2.1"
+ type-check "~0.4.0"
+
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
+lodash.merge@^4.6.2:
+ version "4.6.2"
+ resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
+ integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
+
+make-dir@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz"
+ integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+ dependencies:
+ pify "^4.0.1"
+ semver "^5.6.0"
+
+merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+ dependencies:
+ braces "^3.0.3"
+ picomatch "^2.3.1"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+mime@^1.4.1:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+minimatch@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimatch@^9.0.4:
+ version "9.0.5"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+ integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
+ dependencies:
+ brace-expansion "^2.0.1"
+
+minimist@^1.2.0, minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+ms@2.1.2, ms@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+nanoid@^3.3.4:
+ version "3.3.6"
+ resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz"
+ integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
+ integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+
+needle@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz"
+ integrity sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==
+ dependencies:
+ debug "^3.2.6"
+ iconv-lite "^0.6.3"
+ sax "^1.2.4"
+
+node-domexception@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
+ integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
+
+node-fetch@^3.3.1:
+ version "3.3.1"
+ resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz"
+ integrity sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==
+ dependencies:
+ data-uri-to-buffer "^4.0.0"
+ fetch-blob "^3.1.4"
+ formdata-polyfill "^4.0.10"
+
+object-inspect@^1.12.3, object-inspect@^1.9.0:
+ version "1.12.3"
+ resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz"
+ integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+
+object-keys@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.4:
+ version "4.1.4"
+ resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz"
+ integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ has-symbols "^1.0.3"
+ object-keys "^1.1.1"
+
+object.values@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz"
+ integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+optionator@^0.9.3:
+ version "0.9.4"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
+ integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
+ dependencies:
+ deep-is "^0.1.3"
+ fast-levenshtein "^2.0.6"
+ levn "^0.4.1"
+ prelude-ls "^1.2.1"
+ type-check "^0.4.0"
+ word-wrap "^1.2.5"
+
+p-limit@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
+parent-module@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
+ integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+ dependencies:
+ callsites "^3.0.0"
+
+parse-node-version@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz"
+ integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz"
+ integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+postcss@^8.4.21:
+ version "8.4.21"
+ resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz"
+ integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
+ dependencies:
+ nanoid "^3.3.4"
+ picocolors "^1.0.0"
+ source-map-js "^1.0.2"
+
+prelude-ls@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
+ integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+prettier-linter-helpers@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+ integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+ dependencies:
+ fast-diff "^1.1.2"
+
+prettier@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
+ integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
+
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz"
+ integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==
+
+punycode@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz"
+ integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+regexp.prototype.flags@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz"
+ integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ functions-have-names "^1.2.2"
+
+regexpp@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz"
+ integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
+
+resolve-from@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
+ integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve@^1.22.1:
+ version "1.22.2"
+ resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz"
+ integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
+ dependencies:
+ is-core-module "^2.11.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
+ integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
+
+rollup@^3.18.0:
+ version "3.20.2"
+ resolved "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz"
+ integrity sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+safe-regex-test@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz"
+ integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.3"
+ is-regex "^1.1.4"
+
+"safer-buffer@>= 2.1.2 < 3.0.0":
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+semver@^5.6.0:
+ version "5.7.1"
+ resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz"
+ integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+semver@^7.0.0, semver@^7.3.8:
+ version "7.3.8"
+ resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz"
+ integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
+ dependencies:
+ lru-cache "^6.0.0"
+
+semver@^7.6.0:
+ version "7.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
+ integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+side-channel@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz"
+ integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+ dependencies:
+ call-bind "^1.0.0"
+ get-intrinsic "^1.0.2"
+ object-inspect "^1.9.0"
+
+socket.io-client@^4.6.1:
+ version "4.6.1"
+ resolved "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz"
+ integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.2"
+ engine.io-client "~6.4.0"
+ socket.io-parser "~4.2.1"
+
+socket.io-parser@~4.2.1:
+ version "4.2.2"
+ resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz"
+ integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+
+source-map-js@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
+ integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+source-map@~0.6.0:
+ version "0.6.1"
+ resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+string.prototype.trim@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz"
+ integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+string.prototype.trimend@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz"
+ integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+string.prototype.trimstart@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz"
+ integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
+ integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+
+strip-json-comments@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+synckit@^0.11.0:
+ version "0.11.4"
+ resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.4.tgz#48972326b59723fc15b8d159803cf8302b545d59"
+ integrity sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==
+ dependencies:
+ "@pkgr/core" "^0.2.3"
+ tslib "^2.8.1"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+ts-api-utils@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91"
+ integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
+
+tsconfig-paths@^3.14.1:
+ version "3.14.2"
+ resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz"
+ integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==
+ dependencies:
+ "@types/json5" "^0.0.29"
+ json5 "^1.0.2"
+ minimist "^1.2.6"
+ strip-bom "^3.0.0"
+
+tslib@^2.3.0:
+ version "2.5.0"
+ resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz"
+ integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
+
+tslib@^2.8.1:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
+type-check@^0.4.0, type-check@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
+ integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+ dependencies:
+ prelude-ls "^1.2.1"
+
+typed-array-length@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz"
+ integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==
+ dependencies:
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ is-typed-array "^1.1.9"
+
+unbox-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz"
+ integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
+ dependencies:
+ call-bind "^1.0.2"
+ has-bigints "^1.0.2"
+ has-symbols "^1.0.3"
+ which-boxed-primitive "^1.0.2"
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+uuid@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz"
+ integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
+
+vite@^4.2.0:
+ version "4.2.1"
+ resolved "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz"
+ integrity sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==
+ dependencies:
+ esbuild "^0.17.5"
+ postcss "^8.4.21"
+ resolve "^1.22.1"
+ rollup "^3.18.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+web-streams-polyfill@^3.0.3:
+ version "3.2.1"
+ resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz"
+ integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
+
+which-boxed-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz"
+ integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+ dependencies:
+ is-bigint "^1.0.1"
+ is-boolean-object "^1.1.0"
+ is-number-object "^1.0.4"
+ is-string "^1.0.5"
+ is-symbol "^1.0.3"
+
+which-typed-array@^1.1.9:
+ version "1.1.9"
+ resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz"
+ integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-tostringtag "^1.0.0"
+ is-typed-array "^1.1.10"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+word-wrap@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
+ integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
+
+ws@~8.11.0:
+ version "8.11.0"
+ resolved "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz"
+ integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
+
+xmlhttprequest-ssl@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz"
+ integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==