diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..107f1a3 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DB_USER=dfvdsdfvsdswvdfvdsvdfvdf +DB_PASSWORD=dvnsevhjeiwurowuqvhjevfvuodfb +DB_NAME=cb \ No newline at end of file diff --git a/BACKEND/.dockerignore b/BACKEND/.dockerignore deleted file mode 100644 index 52bc6f2..0000000 --- a/BACKEND/.dockerignore +++ /dev/null @@ -1,35 +0,0 @@ -# Include any files or directories that you don't want to be copied to your -# container here (e.g., local build artifacts, temporary files, etc.). -# -# For more help, visit the .dockerignore file reference guide at -# https://docs.docker.com/engine/reference/builder/#dockerignore-file - -**/.DS_Store -**/__pycache__ -**/.venv -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/bin -**/charts -**/docker-compose* -**/compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md -data diff --git a/BACKEND/.editorconfig b/BACKEND/.editorconfig deleted file mode 100644 index d41d83b..0000000 --- a/BACKEND/.editorconfig +++ /dev/null @@ -1,27 +0,0 @@ -# EditorConfig is awesome: http://EditorConfig.org - -# top-most EditorConfig file -root = true - -# Unix-style newlines with a newline ending every file -[*] -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true -charset = utf-8 - -# 4 space indentation -[*.{py,java,r,R}] -max_line_length = 79 -indent_style = space -indent_size = 4 - -# 2 space indentation -[*.{js,json,yaml,yml,html,cwl}] -indent_style = space -indent_size = 2 - -[*.{md,Rmd,rst}] -trim_trailing_whitespace = false -indent_style = space -indent_size = 2 \ No newline at end of file diff --git a/BACKEND/.env-example b/BACKEND/.env-example deleted file mode 100644 index 6fba6ac..0000000 --- a/BACKEND/.env-example +++ /dev/null @@ -1,10 +0,0 @@ -DB_HOST=localhost -DB_USERNAME=postgres -DB_PASSWORD=admin -DB_NAME=cb -CHECKER_PORT=7070 -HASH_SALT=salt -REDIS_HOST=localhost -ADMIN_LOGIN=admin -ADMIN_PASSWORD=admin -REQUIRE_CAPTCHA=false diff --git a/BACKEND/.gitignore b/BACKEND/.gitignore deleted file mode 100644 index b0b6f3a..0000000 --- a/BACKEND/.gitignore +++ /dev/null @@ -1,160 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -.idea/ \ No newline at end of file diff --git a/BACKEND/.pylintrc b/BACKEND/.pylintrc deleted file mode 100644 index 7af4bb6..0000000 --- a/BACKEND/.pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[MASTER] -init-hook='import sys; sys.path.append("./src")' \ No newline at end of file diff --git a/BACKEND/Dockerfile b/BACKEND/Dockerfile deleted file mode 100644 index 96397f5..0000000 --- a/BACKEND/Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ -# syntax=docker/dockerfile:1 - -# Comments are provided throughout this file to help you get started. -# If you need more help, visit the Dockerfile reference guide at -# https://docs.docker.com/engine/reference/builder/ - -FROM python:3.9.19-alpine3.20 as base - -# Prevents Python from writing pyc files. -ENV PYTHONDONTWRITEBYTECODE=1 - -# Keeps Python from buffering stdout and stderr to avoid situations where -# the application crashes without emitting any logs due to buffering. -ENV PYTHONUNBUFFERED=1 - -WORKDIR /app - -# Create a non-privileged user that the app will run under. -# See https://docs.docker.com/go/dockerfile-user-best-practices/ -ARG UID=10001 -RUN adduser \ - --disabled-password \ - --gecos "" \ - --home "/nonexistent" \ - --shell "/sbin/nologin" \ - --no-create-home \ - --uid "${UID}" \ - appuser - -# Download dependencies as a separate step to take advantage of Docker's caching. -# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. -# Leverage a bind mount to requirements.txt to avoid having to copy them into -# into this layer. -RUN --mount=type=cache,target=/root/.cache/pip \ - --mount=type=bind,source=requirements.txt,target=requirements.txt \ - python -m pip install -r requirements.txt - -# Switch to the non-privileged user to run the application. -USER appuser - -# Copy the source code into the container. -COPY . . - -# Expose the port that the application listens on. -EXPOSE 8000 - -# Run the application. -CMD cd src && gunicorn --workers=4 -b=0.0.0.0:8000 wsgi:app diff --git a/BACKEND/REDAME.md b/BACKEND/REDAME.md deleted file mode 100644 index c4d56ba..0000000 --- a/BACKEND/REDAME.md +++ /dev/null @@ -1,12 +0,0 @@ -# Backend _(aka Application server)_ - -Used to process web requests - -## Files - -- Postman API Folder `API.postman_collection.json` at codebattles folder - -## Stack - -- Python, Flask -- Gunicorn (wsgi server) diff --git a/BACKEND/requirements.txt b/BACKEND/requirements.txt deleted file mode 100644 index cf27f8b..0000000 --- a/BACKEND/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -requests~=2.31.0 -Flask==3.0.0 -transliterate~=1.10.2 -python-dotenv~=1.0.0 -psycopg2-binary==2.9.9 -Flask-Cors~=4.0.0 -redis~=5.0.1 -captcha~=0.5.0 -gunicorn~=22.0.0 -wtforms~=3.1.2 -wtforms_json~=0.3.5 diff --git a/BACKEND/src/__init__.py b/BACKEND/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/BACKEND/src/app.py b/BACKEND/src/app.py deleted file mode 100644 index 5ba02db..0000000 --- a/BACKEND/src/app.py +++ /dev/null @@ -1,27 +0,0 @@ -import wtforms_json -from flask import Flask, request, make_response - -wtforms_json.init() - -app = Flask(__name__) - - -@app.after_request -def cors_middleware(response): - origin = request.headers.get("Origin") - if request.method == "OPTIONS": - response = make_response() - response.headers.add("Access-Control-Allow-Credentials", "true") - response.headers.add("Access-Control-Allow-Headers", "Content-Type") - response.headers.add("Access-Control-Allow-Headers", "x-csrf-token") - response.headers.add( - "Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE" - ) - if origin: - response.headers.add("Access-Control-Allow-Origin", origin) - else: - response.headers.add("Access-Control-Allow-Credentials", "true") - if origin: - response.headers.add("Access-Control-Allow-Origin", origin) - - return response diff --git a/BACKEND/src/config.py b/BACKEND/src/config.py deleted file mode 100644 index 2572468..0000000 --- a/BACKEND/src/config.py +++ /dev/null @@ -1,2 +0,0 @@ -ADMIN_LOGIN = "admin" -ADMIN_PASSWORD = "admin" diff --git a/BACKEND/src/database/__init__.py b/BACKEND/src/database/__init__.py deleted file mode 100644 index d7ace94..0000000 --- a/BACKEND/src/database/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -import psycopg2 -import env - -from database.createTables import ( - CHAMPS_TABLE, - PROBLEMS_TABLE, - SERVERS_TABLE, - STORAGE_TABLE, - TEACHER_CHAMPS_TABLE, - GLOBALUSERS_TABLE, -) -from database.migrations import sql_migrations - -__tables = [ - CHAMPS_TABLE, - PROBLEMS_TABLE, - SERVERS_TABLE, - STORAGE_TABLE, - TEACHER_CHAMPS_TABLE, - GLOBALUSERS_TABLE, -] - - -def get_connection(): - return psycopg2.connect( - dbname=env.DB_NAME, - user=env.DB_USERNAME, - password=env.DB_PASSWORD, - host=env.DB_HOST, - ) - - -def init_tables(): - connection = get_connection() - cur = connection.cursor() - - for sql in __tables: - cur.execute(sql) - - for sql in sql_migrations: - cur.execute(sql) - - connection.commit() diff --git a/BACKEND/src/database/createTables.py b/BACKEND/src/database/createTables.py deleted file mode 100644 index 24a8d65..0000000 --- a/BACKEND/src/database/createTables.py +++ /dev/null @@ -1,117 +0,0 @@ -CHAMPS_TABLE = """ - CREATE TABLE IF NOT EXISTS champs ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - started TIMESTAMP, - A INTEGER, - B INTEGER, - C INTEGER, - D INTEGER, - E INTEGER, - F INTEGER, - G INTEGER, - H INTEGER, - I INTEGER, - J INTEGER, - K INTEGER - ); - -""" -PROBLEMS_TABLE = """ - CREATE TABLE IF NOT EXISTS problems ( - id SERIAL PRIMARY KEY, - name TEXT, - description TEXT, - "in" TEXT, - "out" TEXT, - tests TEXT, - examples TEXT - ); -""" - -SERVERS_TABLE = """ -CREATE TABLE IF NOT EXISTS servers -( - id SERIAL PRIMARY KEY, - name TEXT, - lang_name TEXT, - address TEXT, - enabled boolean DEFAULT true -) -""" - -STORAGE_TABLE = """ -CREATE TABLE IF NOT EXISTS storage -( - id SERIAL PRIMARY KEY, - key TEXT, - value TEXT -) -""" - - -def get_query_users_table(champ_id): - return f""" -CREATE TABLE champUsers_{champ_id} ( - id SERIAL PRIMARY KEY, - login TEXT, - password TEXT, - name TEXT NOT NULL, - A INTEGER, - B INTEGER, - C INTEGER, - D INTEGER, - E INTEGER, - F INTEGER, - G INTEGER, - H INTEGER, - I INTEGER, - J INTEGER, - K INTEGER, - score INTEGER GENERATED ALWAYS AS (COALESCE(A, 0) - + COALESCE(B, 0) + COALESCE(C, 0) + COALESCE(D, 0) - + COALESCE(E, 0) + COALESCE(F, 0) + COALESCE(G, 0) + COALESCE(H, 0) - + COALESCE(I, 0) + COALESCE(J, 0) + COALESCE(K, 0)) STORED -); - - """ - - -def get_query_sends_table(champ_id): - return f""" - CREATE TABLE champSends_{champ_id} ( - id SERIAL PRIMARY KEY, - problem_letter TEXT, - problem_name TEXT NOT NULL, - problem_id INTEGER, - user_id INTEGER, - send_time TIMESTAMP, - state TEXT, - description TEXT, - program TEXT, - score INTEGER, - lang TEXT - ); - """ - - -TEACHER_CHAMPS_TABLE = """ -CREATE TABLE IF NOT EXISTS teacher_champs -( - id SERIAL PRIMARY KEY, - teacher_id bigint NOT NULL, - champ_id bigint NOT NULL, - "isOwner" boolean NOT NULL DEFAULT true -)""" - -GLOBALUSERS_TABLE = """ -CREATE TABLE IF NOT EXISTS globalusers -( - id SERIAL PRIMARY KEY, - login text NOT NULL, - password text NOT NULL, - role text NOT NULL, - description text, - name text -) -""" diff --git a/BACKEND/src/database/migrations.py b/BACKEND/src/database/migrations.py deleted file mode 100644 index eeb73ee..0000000 --- a/BACKEND/src/database/migrations.py +++ /dev/null @@ -1,11 +0,0 @@ -_0_ADD_TYPE_OF_PROBLEM = """ -ALTER TABLE problems -ADD COLUMN IF NOT EXISTS is_question BOOLEAN; -""" - -_1_ADD_TOTP_CODE = """ -ALTER TABLE globalusers -ADD COLUMN IF NOT EXISTS totp TEXT; -""" - -sql_migrations = [_0_ADD_TYPE_OF_PROBLEM, _1_ADD_TOTP_CODE] diff --git a/BACKEND/src/database/redis/__init__.py b/BACKEND/src/database/redis/__init__.py deleted file mode 100644 index 2e3988c..0000000 --- a/BACKEND/src/database/redis/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import redis - -redis_pool = None - - -def redis_pool_init(): - global redis_pool - - redis_pool = redis.ConnectionPool( - port=6379, - decode_responses=True, - socket_timeout=100, - ) diff --git a/BACKEND/src/database/redis/redisWrapper.py b/BACKEND/src/database/redis/redisWrapper.py deleted file mode 100644 index 559201c..0000000 --- a/BACKEND/src/database/redis/redisWrapper.py +++ /dev/null @@ -1,28 +0,0 @@ -import redis -from redis import RedisError - - -class RedisWrapper: - - def __init__(self, pool, **kwargs): - self.__redis_inited = True - self.__r = redis.Redis(connection_pool=pool, **kwargs) - - self.get = self.wrapper(self.__r.get) - self.set = self.wrapper(self.__r.set) - self.delete = self.wrapper(self.__r.delete) - - def wrapper(self, func): - def inner(*args, **kwargs): - if not self.__redis_inited: - return None - - try: - result = func(*args, **kwargs) - return result - except RedisError as e: - print(e) - self.__redis_inited = False - return None - - return inner diff --git a/BACKEND/src/decorators/__init__.py b/BACKEND/src/decorators/__init__.py deleted file mode 100644 index fdd81dc..0000000 --- a/BACKEND/src/decorators/__init__.py +++ /dev/null @@ -1,158 +0,0 @@ -from functools import wraps - -import psycopg2 -from flask import request, redirect, make_response -from psycopg2.extras import RealDictCursor - -import env -from database import get_connection -from database.redis import redis_pool -from database.redis.redisWrapper import RedisWrapper -from utils import salt_crypt - - -def redis_conn(f): - @wraps(f) - def decorated_function(*args, **kwargs): - r = RedisWrapper(pool=redis_pool, host=env.REDIS_HOST) - return f(*args, **kwargs, r=r) - - return decorated_function - - -def admin_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - admin_login_password = request.cookies.get("admin", None) - is_admin = admin_login_password == f"{env.ADMIN_LOGIN}_{env.ADMIN_PASSWORD}_531" - - if is_admin: - return f(*args, **kwargs) - return redirect("/admin/auth") - - return decorated_function - - -def teacher_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - teacher_login_password = request.cookies.get("teacher", None) - - # print(f"{teacher_login_password=}") - # is_teacher = teacher_login_password == f"{'login'}_{'psswd'}_88416" - login, password, client_hash = teacher_login_password.split("_") - - predicted_hash = salt_crypt(login, password) - - if predicted_hash != client_hash: - return {"success": False, "msg": "Bad Credentials"}, 403 - - # return ",", 403 - - connection = get_connection() - cursor = connection.cursor(cursor_factory=RealDictCursor) - - cursor.execute( - f""" - SELECT id from globalusers - WHERE role = 'TEACHER' - AND password = %s - AND login = %s - """, - (password, login), - ) - - if cursor.fetchone(): - return f(*args, **kwargs) - - return {"success": False, "msg": "Bad Credentials"}, 403 - - return decorated_function - - -def login_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if not request.cookies.get("authed", None) is None: - battle_id = int(request.cookies.get("battle_id")) - user_id = int(request.cookies.get("user_id")) - validation_token = request.cookies.get("__validation") - - is_valid = salt_crypt(battle_id, user_id) == validation_token - - if not is_valid: - return redirect("/") - - connection = get_connection() - cur = connection.cursor() - - try: - cur.execute( - f"SELECT * FROM champUsers_{battle_id} " f"WHERE id = {user_id}" - ) - except psycopg2.errors.UndefinedTable: - return redirect("/logout") - - if cur.fetchone() is None: - return redirect("/logout") - - return f(*args, **kwargs, user_id=battle_id) - - return redirect("/") - - return decorated_function - - -def reset_cookie_and_return_bad_cred(): - resp = make_response({"success": False, "msg": "Bad Credentials"}, 403) - - resp.set_cookie("user_id", expires=0) - resp.set_cookie("authed", expires=0) - resp.set_cookie("battle_id", expires=0) - resp.set_cookie("__validation", expires=0) - - return resp - - -def api_login_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if not request.cookies.get("authed", None) is None: - battle_id = int(request.cookies.get("battle_id")) - user_id = int(request.cookies.get("user_id")) - validation_token = request.cookies.get("__validation") - - is_valid = salt_crypt(battle_id, user_id) == validation_token - - if not is_valid: - return reset_cookie_and_return_bad_cred() - - connection = get_connection() - cur = connection.cursor() - - try: - cur.execute( - f"SELECT * FROM champUsers_{battle_id}" f" WHERE id = {user_id}" - ) - except psycopg2.errors.UndefinedTable: - return reset_cookie_and_return_bad_cred() - - if cur.fetchone() is None: - return reset_cookie_and_return_bad_cred() - return f(*args, **kwargs, user_id=battle_id) - - return reset_cookie_and_return_bad_cred() - - return decorated_function - - -def get_user_id(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if not request.cookies.get("authed", None) is None: - user_id = int(request.cookies.get("user_id")) - return f(*args, **kwargs, uid=user_id) - - return redirect("/") - - return decorated_function diff --git a/BACKEND/src/decorators/validation.py b/BACKEND/src/decorators/validation.py deleted file mode 100644 index 0318958..0000000 --- a/BACKEND/src/decorators/validation.py +++ /dev/null @@ -1,19 +0,0 @@ -from functools import wraps -from typing import Type - -from flask import abort, request -from wtforms import Form - - -def json_validate(clazz: Type[Form]): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - x: Form = clazz.from_json(request.json) - if not x.validate(): - return abort(400) - return f(*args, **kwargs, data=x) - - return decorated_function - - return decorator diff --git a/BACKEND/src/env.py b/BACKEND/src/env.py deleted file mode 100644 index 14950d7..0000000 --- a/BACKEND/src/env.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -DB_HOST = None -DB_USERNAME = None -DB_PASSWORD = None -DB_NAME = None -CHECKER_PORT = None -HASH_SALT = None -REDIS_HOST = None -ADMIN_LOGIN = None -ADMIN_PASSWORD = None -REQUIRE_CAPTCHA = None - - -def init(): - global DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME, CHECKER_PORT, HASH_SALT, REDIS_HOST, ADMIN_LOGIN, ADMIN_PASSWORD, REQUIRE_CAPTCHA - - DB_HOST = os.environ.get("DB_HOST") - DB_USERNAME = os.environ.get("DB_USERNAME") - DB_PASSWORD = os.environ.get("DB_PASSWORD") - DB_NAME = os.environ.get("DB_NAME") - CHECKER_PORT = os.environ.get("CHECKER_PORT") - HASH_SALT = os.environ.get("HASH_SALT") - REDIS_HOST = os.environ.get("REDIS_HOST") - ADMIN_LOGIN = os.environ.get("ADMIN_LOGIN") - ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD") - REQUIRE_CAPTCHA = os.environ.get("REQUIRE_CAPTCHA") - - if REQUIRE_CAPTCHA == "true": - REQUIRE_CAPTCHA = True - else: - REQUIRE_CAPTCHA = False diff --git a/BACKEND/src/main.py b/BACKEND/src/main.py deleted file mode 100644 index 55b7bb4..0000000 --- a/BACKEND/src/main.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -from dotenv import load_dotenv - -import env -from app import * -from database import init_tables -from database.redis import redis_pool_init - -import web - - -def init_env(): - dotenv_path = os.path.join(os.path.dirname(__file__), "../.env") - if os.path.exists(dotenv_path): - load_dotenv(dotenv_path) - - env.init() - redis_pool_init() - - -def webapp(): - init_env() - init_tables() - return app - - -if __name__ == "__main__": - webapp().run(host="0.0.0.0", port=2500, debug=True) diff --git a/BACKEND/src/passwordTools.py b/BACKEND/src/passwordTools.py deleted file mode 100644 index 77d4b38..0000000 --- a/BACKEND/src/passwordTools.py +++ /dev/null @@ -1,9 +0,0 @@ -import random -import string - - -def get_random_string(length): - letters = string.digits + string.ascii_letters - result_str = "".join(random.choice(letters) for _ in range(length)) - - return result_str diff --git a/BACKEND/src/services/__init__.py b/BACKEND/src/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/BACKEND/src/services/captcha_service.py b/BACKEND/src/services/captcha_service.py deleted file mode 100644 index 0f2970a..0000000 --- a/BACKEND/src/services/captcha_service.py +++ /dev/null @@ -1,47 +0,0 @@ -import base64 -import random -from dataclasses import dataclass - -from captcha.image import ImageCaptcha - -from utils import salt_crypt - -numbers_string = "1234567890" - - -@dataclass -class CaptchaOutput: - base64image: str - string: str - - -@dataclass -class CaptchaValidatedOutput(CaptchaOutput): - sha256_hash: str - - -def generate(numbers: int = 6) -> CaptchaOutput: - image = ImageCaptcha() - - string_ref = "".join(random.choice(numbers_string) for _ in range(numbers)) - data = image.generate(string_ref) - encoded_bytes = base64.b64encode(data.getvalue()) - encoded_string = encoded_bytes.decode("utf-8") - - return CaptchaOutput(base64image=encoded_string, string=string_ref) - - -def generate_with_validation(numbers: int = 6) -> CaptchaValidatedOutput: - generated = generate(numbers) - - sha_hash = salt_crypt(generated.string + generated.base64image) - - return CaptchaValidatedOutput( - base64image=generated.base64image, string=generated.string, sha256_hash=sha_hash - ) - - -def validate(base64image, string_, original_hash) -> bool: - user_data_hash = salt_crypt(string_ + base64image) - print(user_data_hash, original_hash) - return user_data_hash == original_hash diff --git a/BACKEND/src/templates/admin/add_users.html b/BACKEND/src/templates/admin/add_users.html deleted file mode 100644 index ac39930..0000000 --- a/BACKEND/src/templates/admin/add_users.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - Document - - - - -
-

Админ панель

-
-
-

Создание пользователей

-

Введите пользователей, каждый на новой строке

- -
- -
-
- - - \ No newline at end of file diff --git a/BACKEND/src/templates/admin/auth.html b/BACKEND/src/templates/admin/auth.html deleted file mode 100644 index 7e607b2..0000000 --- a/BACKEND/src/templates/admin/auth.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Title - - - -
-
- - - - - -
-
- - - \ No newline at end of file diff --git a/BACKEND/src/templates/admin/create.html b/BACKEND/src/templates/admin/create.html deleted file mode 100644 index 350dfa3..0000000 --- a/BACKEND/src/templates/admin/create.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - Document - - - - -
-

Админ панель

-
- -
-

Создать соревнование

-
- - - - - - -
- -
-
- - - \ No newline at end of file diff --git a/BACKEND/src/templates/admin/panel.html b/BACKEND/src/templates/admin/panel.html deleted file mode 100644 index 145d950..0000000 --- a/BACKEND/src/templates/admin/panel.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - Document - - - - -
-

Админ панель

- Создать соревнование -
- - {% for champ in champs %} - -
-
-

{{champ[0]}}

-

{{champ[1]}}

-

        

- Настройки - Участники -
-
-

ХЗ человек

-

Старт в {{champ[2]}}

-

Конец в ХЗ

-
-
- - {% endfor %} - - - - \ No newline at end of file diff --git a/BACKEND/src/templates/admin/problems_list.html b/BACKEND/src/templates/admin/problems_list.html deleted file mode 100644 index 8f22b6d..0000000 --- a/BACKEND/src/templates/admin/problems_list.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - Document - - - - -
-

Админ панель

-
- -
-

Добавить соревнование

- Из TestsGeneratorFramework -
- -

build.json файл

- - -
-
- - -

Соревнования

- - -
- - {% for problem in problems%} - -
-
-

{{problem[0]}}

-

{{problem[1]}}

- -
-
- - {% endfor %} - -
- - - \ No newline at end of file diff --git a/BACKEND/src/templates/admin/settings.html b/BACKEND/src/templates/admin/settings.html deleted file mode 100644 index 32ad5ce..0000000 --- a/BACKEND/src/templates/admin/settings.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - Document - - - - -
-

Админ панель

-
-

ID

-

{{id}}

-

{{name}}

-

Тестоовое супер нужное и важное название

-
-
- - Управление пользователями - -
- - - - - - - - - - - {% for task in tasks %} - - - - - - {% endfor %} - -
Букваid задачиНазвание задачи
{{task[0]}}{{task[1]}}{{task[2]}}
-
-
- -
- - - - - -

Поставьте id -1 чтобы удалить задачу

- -
-
- - - \ No newline at end of file diff --git a/BACKEND/src/templates/admin/users.html b/BACKEND/src/templates/admin/users.html deleted file mode 100644 index 4b69b70..0000000 --- a/BACKEND/src/templates/admin/users.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - Document - - - - -
-

Админ панель

-
-

ID

-

{{id}}

-
-
- - Добавить пользователей - -
- - - - - - - - - - - {% for task in users %} - - - - - - {% endfor %} - -
ИмяЛогинПароль
{{task[0]}}{{task[1]}}{{task[2]}}
-
- - - \ No newline at end of file diff --git a/BACKEND/src/utils.py b/BACKEND/src/utils.py deleted file mode 100644 index f032ce0..0000000 --- a/BACKEND/src/utils.py +++ /dev/null @@ -1,46 +0,0 @@ -import re -from hashlib import sha256 -import env - - -def salt_crypt(*args): - args = list(map(str, args)) - payload = f"{env.HASH_SALT}+++{str(args)}" - return sha256(payload.encode("utf-8")).hexdigest() - - -def fix_new_line(data): - print(data) - if isinstance(data, list): - out = [] - for i in data: - out.append(fix_new_line(i)) - return out - elif isinstance(data, str): - return data.replace("\\n", "\n") - else: - return data - - -def get_table_color_class_by_score(score): - if score is None: - return "" - elif score == 100: - return "table-success" - - elif score == 0: - return "table-danger" - - return "table-warning" - - -def get_table_color_class_by_test_message(msg): - if msg == "OK" or msg == "SUCCESS": - return "table-success" - elif msg == "WRONG_ANSWER": - return "table-danger" - - return "table-info" - - -LETTER_REGEX = re.compile(r"[A-Z]") diff --git a/BACKEND/src/web/__init__.py b/BACKEND/src/web/__init__.py deleted file mode 100644 index baa374a..0000000 --- a/BACKEND/src/web/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -import web.api -import web.teacher_api -import web.admin - -import web.solution_processing -import web.error_handlers diff --git a/BACKEND/src/web/admin/__init__.py b/BACKEND/src/web/admin/__init__.py deleted file mode 100644 index 392ff91..0000000 --- a/BACKEND/src/web/admin/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import web.admin.panel -import web.admin.auth -import web.admin.problems diff --git a/BACKEND/src/web/admin/auth.py b/BACKEND/src/web/admin/auth.py deleted file mode 100644 index 53278d4..0000000 --- a/BACKEND/src/web/admin/auth.py +++ /dev/null @@ -1,21 +0,0 @@ -from flask import render_template, request, redirect, make_response - -from app import app - - -@app.route("/admin/auth") -def admin_auth(): - return render_template("admin/auth.html") - - -@app.route("/admin/auth", methods=["POST"]) -def admin_auth_post(): - login, password = request.form["login"], request.form["password"] - - resp = make_response(redirect("/admin")) - - resp.set_cookie("admin", f"{login}_{password}_531") - - print(login, password) - - return resp diff --git a/BACKEND/src/web/admin/panel.py b/BACKEND/src/web/admin/panel.py deleted file mode 100644 index 275e449..0000000 --- a/BACKEND/src/web/admin/panel.py +++ /dev/null @@ -1,171 +0,0 @@ -import random -import string - -from flask import render_template, redirect, request - -from app import app -from database import get_connection -from database.createTables import get_query_users_table, get_query_sends_table -from decorators import admin_required - - -@app.route("/admin") -@admin_required -def admin_panel(): - connection = get_connection() - cursor = connection.cursor() - - cursor.execute("SELECT id, name, started FROM champs ") - - champs = cursor.fetchall() - champs = list(map(lambda x: [*x], champs)) - - return render_template("admin/panel.html", champs=champs) - - -@app.route("/admin/champ/") -@admin_required -def settings(champ_id): - connection = get_connection() - cur = connection.cursor() - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(champ_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_ids = [] - - sql = "SELECT * FROM problems WHERE id = -1 " - - strs = string.ascii_uppercase - - for task in problems_ids_temp: - if task is not None: - problems_ids.append(task) - sql += "OR id = %s " - - cur.execute(sql, tuple(problems_ids)) - - tasks_dict = dict.fromkeys(problems_ids) - - x = list(cur.fetchall()) - - tasks = [] - - for i, task in enumerate(x): - task_id = task[0] - name = task[1] - tasks_dict[task_id] = task - tasks.append((strs[problems_ids.index(task_id)], task_id, name)) - - tasks.sort() - - print() - - return render_template( - "admin/settings.html", tasks=tasks, id=fetch[0], name=fetch[1] - ) - - -@app.route("/admin/champ/", methods=["POST"]) -@admin_required -def settings_post(champ_id): - connection = get_connection() - cur = connection.cursor() - - form = request.form - problem = form["problem"] - problem_id = form["problem_id"] - - if problem_id == "": - return redirect(f"/admin/champ/{champ_id}") - problem_id = int(problem_id) - - print(problem, problem_id) - - cur.execute(f"""UPDATE champs SET {problem} = {problem_id} WHERE id = {champ_id}""") - - connection.commit() - - return redirect(f"/admin/champ/{champ_id}") - - -@app.route("/admin/create/") -@admin_required -def create_champ(): - return render_template("admin/create.html") - - -@app.route("/admin/create/", methods=["POST"]) -@admin_required -def create_champ_post(): - name = request.form["name"] - - connection = get_connection() - cur = connection.cursor() - - cur.execute("INSERT INTO champs (name) VALUES (%s)", (name,)) - - connection.commit() - - cur.execute("SELECT currval(pg_get_serial_sequence('champs','id'));") - champ_id = cur.fetchone()[0] - - cur.execute(get_query_users_table(champ_id)) - cur.execute(get_query_sends_table(champ_id)) - - connection.commit() - - return redirect("/admin") - - -@app.route("/admin/champ//add_users") -@admin_required -def create_users_in_champ(champ_id): - return render_template("admin/add_users.html") - - -@app.route("/admin/champ//add_users", methods=["POST"]) -@admin_required -def create_users_in_champ_post(champ_id): - users = request.form["users"].split("\r\n") - - connection = get_connection() - cursor = connection.cursor() - - for name in users: - # login = translit(name, 'ru', reversed=True) - # login = login.replace(" ", "") - # login = login.replace("'", "") - # if len(login) > 7: - # login = login[:6] - # - # login = f"{login}{random.randint(10, 99)}" - # - # password = get_random_string(8) - - login = "".join(map(str, [random.randint(0, 9) for _ in range(5)])) - password = "".join(map(str, [random.randint(0, 9) for _ in range(5)])) - - cursor.execute( - f"INSERT INTO champUsers_{champ_id} (login, password, name)" - f" VALUES (%s, %s, %s)", - (login, password, name), - ) - - connection.commit() - - return redirect(f"/admin/champ/{champ_id}/users") - - -@app.route("/admin/champ//users") -@admin_required -def users_route(champ_id): - connection = get_connection() - cur = connection.cursor() - - cur.execute(f"SELECT name, login, password FROM champUsers_{champ_id}") - - users = cur.fetchall() - - return render_template("admin/users.html", users=users, id=champ_id) diff --git a/BACKEND/src/web/admin/problems.py b/BACKEND/src/web/admin/problems.py deleted file mode 100644 index deee7b3..0000000 --- a/BACKEND/src/web/admin/problems.py +++ /dev/null @@ -1,63 +0,0 @@ -import json - -from flask import render_template, request - -from app import app -from database import get_connection -from decorators import admin_required - - -@app.route("/admin/problems") -@admin_required -def admin_list_problems(): - connection = get_connection() - cursor = connection.cursor() - - cursor.execute("SELECT id, name, description FROM problems") - - problems = cursor.fetchall() - - problems = list(map(lambda problem: [*problem], problems)) - - return render_template("admin/problems_list.html", problems=problems) - - -@app.route("/admin/problems/add", methods=["POST"]) -@admin_required -def admin_list_problems_add(): - connection = get_connection() - cursor = connection.cursor() - - cursor.execute("SELECT id, name, description FROM problems") - - problems = cursor.fetchall() - - problems = list(map(lambda problem: [*problem], problems)) - - build = request.form["build"] - try: - build_json = json.loads(build) - build_json["tests"] = json.dumps(build_json["tests"]) - build_json["examples"] = json.dumps(build_json["examples"]) - except Exception as e: - print("Maybe Json parse exception \n" + str(e)) - return "Not json (404 ERR)", 400 - - print(build) - print(build_json) - - cursor.execute( - """INSERT INTO problems (name, description, "in", out, examples, tests) VALUES (%s, %s, %s, %s, %s, %s) """, - ( - build_json["name"], - build_json["description"], - build_json["in"], - build_json["out"], - build_json["examples"], - build_json["tests"], - ), - ) - - connection.commit() - - return render_template("admin/problems_list.html", problems=problems) diff --git a/BACKEND/src/web/api/__init__.py b/BACKEND/src/web/api/__init__.py deleted file mode 100644 index 6d7d143..0000000 --- a/BACKEND/src/web/api/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -import web.api.send_prog -import web.api.sends -import web.api.battle -import web.api.quiz_problems -import web.api.auth diff --git a/BACKEND/src/web/api/auth.py b/BACKEND/src/web/api/auth.py deleted file mode 100644 index 11ad6f9..0000000 --- a/BACKEND/src/web/api/auth.py +++ /dev/null @@ -1,54 +0,0 @@ -from flask import request, make_response - -from app import app -from database import get_connection -from decorators.validation import json_validate -from utils import salt_crypt -from web.validation_form.api import LoginForm - - -@app.route("/api/logout", methods=["POST", "GET"]) -def logout_api(): - resp = make_response({"success": True}) - - resp.set_cookie("user_id", expires=0) - resp.set_cookie("authed", expires=0) - resp.set_cookie("battle_id", expires=0) - - return resp - - -@app.route("/api/login", methods=["POST"]) -@json_validate(LoginForm) -def login_post_api(data: LoginForm): - try: - champ_id = data.id.data - login = data.login.data - password = data.password.data - - champ_id = int(champ_id) - - con = get_connection() - cur = con.cursor() - cur.execute( - f"SELECT * FROM public.champUsers_{champ_id}" - f" WHERE login = %s AND password = %s", - (login, password), - ) - user = cur.fetchone() - - assert user is not None - - user_id = str(user[0]) - - resp = make_response({"success": True}) - resp.set_cookie("user_id", user_id) - resp.set_cookie("authed", str(True)) - resp.set_cookie("battle_id", str(champ_id)) - resp.set_cookie("__validation", salt_crypt(champ_id, user_id)) - - return resp - except Exception as e: - print(e) - - return {"success": False, "msg": "Bad Credentials"}, 403 diff --git a/BACKEND/src/web/api/battle.py b/BACKEND/src/web/api/battle.py deleted file mode 100644 index 7b66ec1..0000000 --- a/BACKEND/src/web/api/battle.py +++ /dev/null @@ -1,220 +0,0 @@ -import json -import re -import string - -from flask import abort - -from app import app -from database import get_connection -from decorators import get_user_id, api_login_required, redis_conn -from utils import fix_new_line, get_table_color_class_by_score, LETTER_REGEX - -JSON_MIMETYPE = "application/json" - - -@app.route("/api/problems") -@api_login_required -@get_user_id -def api_problems(user_id, uid): - connection = get_connection() - cur = connection.cursor() - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(user_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_ids = [] - - sql = "SELECT * FROM problems WHERE id = -1 " - - strs = string.ascii_uppercase - - for task in problems_ids_temp: - if task is not None: - problems_ids.append(task) - sql += "OR id = %s " - - cur.execute(sql, tuple(problems_ids)) - - tasks_dict = dict.fromkeys(problems_ids) - - x = list(cur.fetchall()) - - tasks = {} - is_quizes = {} - - cur.execute(f"SELECT * FROM champUsers_{user_id} WHERE id = %s", (uid,)) - - fetch = cur.fetchone() # Can be None - - score = fetch[4 : 4 + len(problems_ids)] - - css_colors = {} - - for i, task in enumerate(x): - id = task[0] - name = task[1] - is_quiz = task[-1] - if is_quiz is None: - is_quiz = False - - tasks_dict[id] = task - - letter = strs[problems_ids.index(id)] - - tasks[letter] = name - is_quizes[letter] = is_quiz - - css_colors[letter] = get_table_color_class_by_score(score[strs.index(letter)]) - - print() - - return { - "success": "true", - "problems": tasks, - "colors": css_colors, - "is_quizes": is_quizes, - } - - -@app.route("/api/problem/") -@api_login_required -def api_problem(letter, user_id): - if not re.fullmatch(LETTER_REGEX, letter): - return abort(404) - - connection = get_connection() - cur = connection.cursor() - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(user_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_ids = [] - - sql = "SELECT * FROM problems WHERE id = -1 " - - strs = string.ascii_uppercase - - for i in problems_ids_temp: - if i is not None: - problems_ids.append(i) - sql += "OR id = %s " - - cur.execute(sql, tuple(problems_ids)) - - x = list(cur.fetchall()) - - problem_ = None - - for i in x: - problem_id = i[0] - problem_letter = strs[problems_ids.index(problem_id)] - if letter == problem_letter: - problem_ = i - - if problem_ is None: - abort(404) - - cur.execute("SELECT name, id FROM servers WHERE enabled=true") - servers = cur.fetchall() - - _pr_id, _pr_name, _pr_desc, _pr_in, _pr_out, _pr_tests, _pr_examples, _ = problem_ - - _pr_examples = fix_new_line(json.loads(_pr_examples)) - - langs = {} - - for i in servers: - langs[i[0]] = i[1] - - return dict( - success=True, - name=_pr_name, - description=_pr_desc, - letter=letter, - in_data=_pr_in, - out_data=_pr_out, - examples=_pr_examples, - langs=langs, - ) - - -@app.route("/api/stats") -@api_login_required -@get_user_id -@redis_conn -def api_statistics(user_id, uid, r): - champ_id = user_id - - redis_cache = r.get(f"r-champ-{champ_id}-stats") - if redis_cache is not None: - response = app.response_class( - response=redis_cache, status=200, mimetype=JSON_MIMETYPE - ) - print("redis") - return response - - connection = get_connection() - cur = connection.cursor() - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(user_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_counts = 0 - - for i in problems_ids_temp: - if i is not None: - problems_counts += 1 - else: - break - - strs = string.ascii_uppercase - - print(problems_counts) - - cur.execute( - f""" - SELECT u.*, MAX(s.send_time) AS send_time - FROM champusers_{user_id} u - LEFT JOIN champsends_{user_id} s ON u.id = s.user_id - GROUP BY u.id - ORDER BY score DESC, send_time ASC; - """ - ) - - fetch = cur.fetchall() # Can be None - - users = [] - - for i, usr in enumerate(fetch): - user_id = usr[0] - score = usr[15] - last_send = usr[16] - last_send = ( - None if last_send is None else last_send.strftime("%m/%d/%Y, %H:%M:%S") - ) - - nickname = usr[3] - problems_score = usr[4 : problems_counts + 4] - - # problems_score = list(map(lambda s: (s, "")[s is None], problems_score)) - - users.append( - { - "position": i + 1, - "name": nickname, - "user_id": user_id, - "score": score, - "problems_score": problems_score, - "last_send": last_send, - } - ) - - print() - - resp_string = {"success": True, "cols": strs[:problems_counts], "users": users} - - r.set(f"r-champ-{champ_id}-stats", json.dumps(resp_string)) - return resp_string diff --git a/BACKEND/src/web/api/quiz_problems.py b/BACKEND/src/web/api/quiz_problems.py deleted file mode 100644 index 5d541d2..0000000 --- a/BACKEND/src/web/api/quiz_problems.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -import re -import string - -from flask import abort - -from app import app -from database import get_connection -from decorators import get_user_id, api_login_required, redis_conn -from utils import fix_new_line, get_table_color_class_by_score, LETTER_REGEX - -JSON_MIMETYPE = "application/json" - - -@app.route("/api/problem//quiz") -@api_login_required -def get_quiz(letter, user_id): - # if not re.fullmatch(LETTER_REGEX, letter): - # return abort(404) - - connection = get_connection() - cur = connection.cursor() - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(user_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_ids = [] - - sql = "SELECT * FROM problems WHERE id = -1 " - - strs = string.ascii_uppercase - - for i in problems_ids_temp: - if i is not None: - problems_ids.append(i) - sql += "OR id = %s " - - cur.execute(sql, tuple(problems_ids)) - - x = list(cur.fetchall()) - - problem_ = None - - for i in x: - problem_id = i[0] - problem_letter = strs[problems_ids.index(problem_id)] - if letter == problem_letter: - problem_ = i - - if problem_ is None: - abort(404) - - _pr_id, _pr_name, _pr_desc, _pr_in, _pr_out, _pr_tests, _pr_examples, _ = problem_ - - print(_pr_tests) - _pr_tests = json.loads(_pr_tests) - filtered_test = [] - - for question in _pr_tests["questions"]: - print(question) - filtered_test.append( - { - "id": question["id"], - "name": question["question"], - "answers": question["answers"], - "type": question["type"], - } - ) - - return dict( - success=True, - name=_pr_name, - description=_pr_desc, - letter=letter, - tests=filtered_test, - ) diff --git a/BACKEND/src/web/api/send_prog.py b/BACKEND/src/web/api/send_prog.py deleted file mode 100644 index c3abc1e..0000000 --- a/BACKEND/src/web/api/send_prog.py +++ /dev/null @@ -1,106 +0,0 @@ -import datetime -import json -import string - -import requests - -import env -from app import app -from database import get_connection -from decorators import get_user_id, api_login_required -from decorators.validation import json_validate -from web.validation_form.api import SendProgramForm - - -@app.route("/api/send", methods=["POST"]) -@api_login_required -@get_user_id -@json_validate(SendProgramForm) -def api_send_prog(user_id, uid, data: SendProgramForm): - connection = get_connection() - cur = connection.cursor() - - f_lang = data.cars.data - f_code = data.src.data - problem_letter_form = data.problem.data - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(user_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_ids = [] - - sql = "SELECT * FROM problems WHERE id = -1 " - - strs = string.ascii_uppercase - - for i in problems_ids_temp: - if i is not None: - problems_ids.append(i) - sql += "OR id = %s " - - cur.execute(sql, tuple(problems_ids)) - - x = list(cur.fetchall()) - - problem_ = None - - for i in x: - _id = i[0] - problem_letter = strs[problems_ids.index(_id)] - if problem_letter_form == problem_letter: - problem_ = i - - tests = problem_[5] - tests = json.loads(tests) - tests = list(map(lambda z: {"in": z[0], "out": z[1]}, tests)) - - print(problem_) - - cur.execute( - f""" - INSERT INTO champSends_{user_id} - (problem_name, problem_id, user_id, send_time, state, program, problem_letter, lang) - VALUES(%s, %s, %s, %s, %s, %s, %s, %s); - """, - ( - problem_[1], - problem_[0], - uid, - datetime.datetime.now(), - "Тестируется", - f_code, - problem_letter_form, - f_lang, - ), - ) - cur.execute(f"SELECT currval(pg_get_serial_sequence('champSends_{user_id}','id'));") - - inserted_id = cur.fetchone()[0] - - meta = { - "champ_id": user_id, - "user_id": uid, - "problem": problem_letter_form, - "id": inserted_id, - } - - payload = { - "meta": json.dumps(meta), - "source": f_code, - "compiler": f_lang, - "tests": tests, - } - - connection.commit() - - cur.execute( - f"SELECT address FROM servers WHERE id = %s and enabled = true", (f_lang,) - ) - - server_addr = cur.fetchone() - server_addr = server_addr[0] - print() - - requests.post(f"http://{server_addr}:{env.CHECKER_PORT}/api/v1/test", json=payload) - return {"success": True} diff --git a/BACKEND/src/web/api/sends.py b/BACKEND/src/web/api/sends.py deleted file mode 100644 index 7ad135a..0000000 --- a/BACKEND/src/web/api/sends.py +++ /dev/null @@ -1,124 +0,0 @@ -import json - -from app import app -from database import get_connection -from decorators import get_user_id, api_login_required, redis_conn - - -@app.route("/api/sends") -@api_login_required -@get_user_id -@redis_conn -def api_sends(user_id, uid, r): - champ_id = user_id - - redis_cache = r.get(f"r-champ-{champ_id}-sends-user-{uid}") - if redis_cache is not None: - response = app.response_class( - response=redis_cache, status=200, mimetype="application/json" - ) - - return response - - connection = get_connection() - cur = connection.cursor() - - cur.execute( - f"SELECT * FROM champSends_{user_id} WHERE user_id = %s ORDER BY send_time DESC", - (uid,), - ) - - db_sends = cur.fetchall() - - to_render = [] - - for send in db_sends: - ( - id, - letter, - name, - problem_id, - pr_user_id, - send_time, - state, - result, - program, - score, - lang, - ) = send - - human_send_time = send_time.strftime("%m/%d/%Y, %H:%M:%S") - - to_render.append( - dict( - id=id, - letter=letter, - name=name, - send_time=human_send_time, - state=state, - score=(score, "")[score is None], - program_checked=result is not None, - ) - ) - - print() - - dict_resp = dict(success=True, sends=to_render) - - r.set(f"r-champ-{champ_id}-sends-user-{uid}", json.dumps(dict_resp)) - - return dict_resp - - -@app.route("/api/send/") -@api_login_required -def api_send_viewer(send_id, user_id): - connection = get_connection() - cur = connection.cursor() - - cur.execute( - f""" - SELECT t1.*, t2.name as lang_name, t2.lang_name as lang_id - FROM public.champSends_{user_id} as t1 - INNER JOIN public.servers as t2 - on CAST(t1.lang as INTEGER) = t2.id - - WHERE t1.id = %s - """, - (send_id,), - ) - - data = cur.fetchone() - - if data is None: - return {}, 404 - - result = json.loads(data[7]) - - prog = data[8] - lang = data[11] - lang_id = data[12] - - to_render = [] - - for i, test in enumerate(result): - message = test["msg"] - out = test["out"] - - if message == "WRONG_ANSWER": - out = """ВЫВОД СКРЫТ""" - - to_add = {"id": i + 1, "time": test["time"], "msg": message, "out": out} - to_render.append(to_add) - - connection.close() - - print() - - return { - "success": True, - "tests": to_render, - "lang": lang, - "program": prog, - "lang_id": lang_id, - } diff --git a/BACKEND/src/web/error_handlers.py b/BACKEND/src/web/error_handlers.py deleted file mode 100644 index b3507fe..0000000 --- a/BACKEND/src/web/error_handlers.py +++ /dev/null @@ -1,9 +0,0 @@ -from app import app - - -@app.errorhandler(404) -@app.errorhandler(403) -@app.errorhandler(405) -@app.errorhandler(500) -def resource_not_found(e): - return {"success": False, "status": e.code, "error": str(e)}, e.code diff --git a/BACKEND/src/web/solution_processing/__init__.py b/BACKEND/src/web/solution_processing/__init__.py deleted file mode 100644 index 881f412..0000000 --- a/BACKEND/src/web/solution_processing/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -import web.solution_processing.checker_api -import web.solution_processing.quiz_api diff --git a/BACKEND/src/web/solution_processing/checker_api.py b/BACKEND/src/web/solution_processing/checker_api.py deleted file mode 100644 index e21ab11..0000000 --- a/BACKEND/src/web/solution_processing/checker_api.py +++ /dev/null @@ -1,160 +0,0 @@ -import json -import re -import string -import datetime - -import requests -from flask import redirect, request - -from app import app -from database import get_connection -from decorators import login_required, get_user_id, redis_conn -import env - - -@app.route("/send", methods=["POST"]) -@login_required -@get_user_id -def send_prog(user_id, uid): - connection = get_connection() - cur = connection.cursor() - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(user_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_ids = [] - - sql = "SELECT * FROM problems WHERE id = -1 " - - strs = string.ascii_uppercase - - for i in problems_ids_temp: - if i is not None: - problems_ids.append(i) - sql += "OR id = %s " - - cur.execute(sql, tuple(problems_ids)) - - x = list(cur.fetchall()) - - problem_ = None - problem_letter_form = request.form["problem"] - - for i in x: - _id = i[0] - problem_letter = strs[problems_ids.index(_id)] - if problem_letter_form == problem_letter: - problem_ = i - - tests = problem_[5] - tests = json.loads(tests) - tests = list(map(lambda z: {"in": z[0], "out": z[1]}, tests)) - - print(problem_) - - f_lang = request.form["cars"] - f_code = request.form["src"] - - cur.execute( - f""" - INSERT INTO champSends_{user_id} - (problem_name, problem_id, user_id, send_time, state, program, problem_letter, lang) - VALUES(%s, %s, %s, %s, %s, %s, %s, %s); - """, - ( - problem_[1], - problem_[0], - uid, - datetime.datetime.now(), - "Тестируется", - f_code, - problem_letter_form, - f_lang, - ), - ) - cur.execute(f"SELECT currval(pg_get_serial_sequence('champSends_{user_id}','id'));") - - inserted_id = cur.fetchone()[0] - - meta = { - "champ_id": user_id, - "user_id": uid, - "problem": request.form["problem"], - "id": inserted_id, - } - - data = { - "meta": json.dumps(meta), - "source": request.form["src"], - "compiler": request.form["cars"], - "tests": tests, - } - - connection.commit() - - print(cur.fetchone()) - - cur.execute(f"SELECT address FROM servers WHERE id = %s", (request.form["cars"],)) - - server_addr = cur.fetchone()[0] - - print() - - requests.post( - f"http://{server_addr}:{env.CHECKER_PORT}/api/v1/test", json=data, timeout=1000 - ) - return redirect("/sends") - - -@app.route("/api/check_system_callback", methods=["POST"]) -@redis_conn -def check_system(r): - data = request.json - print(data) - all_count = 0 - correct_count = 0 - for results in data["results"]: - all_count += 1 - if results["success"]: - correct_count += 1 - - meta = json.loads(data["meta"]) - - print(round((correct_count / all_count) * 100)) - - champ_id = meta["champ_id"] - user_id = meta["user_id"] - column = meta["problem"][0] - - if not re.fullmatch("[0-9]+", str(champ_id)): - return "", 409 - if not re.fullmatch("[0-9]+", str(user_id)): - return "", 409 - if not re.fullmatch("[a-zA-Z]", str(column)): - return "", 409 - - con = get_connection() - cur = con.cursor() - - points = round((correct_count / all_count) * 100) - - cur.execute( - f"UPDATE champUsers_{champ_id} SET {column} = {points} \ - WHERE id= {user_id} AND ({column} < {points} OR {column} IS NULL)" - ) - - result_str = json.dumps(data["results"], indent=2) - - print(result_str) - - cur.execute( - f"UPDATE champSends_{champ_id} SET score = %s,state = 'Протестировано', description = %s WHERE id = %s;", - (round((correct_count / all_count) * 100), result_str, meta["id"]), - ) - - con.commit() - - r.delete(f"r-champ-{champ_id}-stats", f"r-champ-{champ_id}-sends-user-{user_id}") - - return "OK" diff --git a/BACKEND/src/web/solution_processing/quiz_api.py b/BACKEND/src/web/solution_processing/quiz_api.py deleted file mode 100644 index 8e53534..0000000 --- a/BACKEND/src/web/solution_processing/quiz_api.py +++ /dev/null @@ -1,110 +0,0 @@ -import datetime -import json -import re - -from flask import request -from psycopg2.extras import RealDictCursor - -from app import app -from database import get_connection -from decorators import login_required, get_user_id, redis_conn - - -@app.route("/api/send/quiz", methods=["POST"]) -@login_required -@get_user_id -@redis_conn -def send_quiz_solution(user_id, uid, r): - connection = get_connection() - cur = connection.cursor() - cur_dict = connection.cursor(cursor_factory=RealDictCursor) - - answers: dict - problem, answers = request.json["problem"], request.json["answers"] - - if not re.fullmatch("[a-zA-Z]", problem): - return "", 409 - - cur.execute(f"SELECT {problem.lower()} FROM champs WHERE id = %s", (str(user_id),)) - - fetch = cur.fetchone() # Can be None - problem_id = fetch[0] - print(problem_id) - - cur_dict.execute(f"SELECT tests, name FROM problems WHERE id = %s", (problem_id,)) - fetch = cur_dict.fetchone() - tests = fetch["tests"] - problem_name = fetch["name"] - - tests_dict = json.loads(tests) - questionById = {} - qustionsCount = 0 - for question in tests_dict["questions"]: - questionById[question["id"]] = question - qustionsCount += 1 - print(questionById) - - points = 0 - report = "" - for answer_id, answer in answers.items(): - current_answer = answer[0] - print(questionById) - correct_answer = questionById[int(answer_id)]["correct_answers"][0] - print(correct_answer) - report += "=" * 30 + "\n" - report += "Expected: " + "\n" - report += str(correct_answer) + "\n" - report += "Answered: " + "\n" - report += str(current_answer) + "\n" - - if current_answer == correct_answer: - points += 1 - report += f"Got 1 point" + "\n" - else: - report += f"Got 0 point" + "\n" - - report += "=" * 30 + "\n" - - totalPoints = int(points / qustionsCount * 100) - - report += "\n" - report += f"Final points: {points}/{qustionsCount} => {totalPoints}" - report += "\n" - - cur.execute( - f"""SELECT id FROM champSends_{user_id} where user_id={uid} and problem_id={problem_id}""" - ) - equals_sends = cur.fetchall() - print("!!!!!!", equals_sends) - - if len(equals_sends) == 0: - - cur.execute( - f""" - INSERT INTO champSends_{user_id} - (problem_name, problem_id, user_id, send_time, state, program, problem_letter, score) - VALUES(%s, %s, %s, %s, %s, %s, %s, %s); - """, - ( - problem_name, - problem_id, - uid, - datetime.datetime.now(), - "Подсчитано", - report, - problem, - totalPoints, - ), - ) - - cur.execute( - f""" - UPDATE champUsers_{user_id} SET {problem.lower()} = {totalPoints} - WHERE id= {uid}""" - ) - - connection.commit() - - r.delete(f"r-champ-{user_id}-stats", f"r-champ-{user_id}-sends-user-{uid}") - - return {"ok": "ok"} diff --git a/BACKEND/src/web/teacher_api/__init__.py b/BACKEND/src/web/teacher_api/__init__.py deleted file mode 100644 index c6638cb..0000000 --- a/BACKEND/src/web/teacher_api/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -import web.teacher_api.auth -import web.teacher_api.champs -import web.teacher_api.students -import web.teacher_api.problems -import web.teacher_api.students_sends diff --git a/BACKEND/src/web/teacher_api/auth.py b/BACKEND/src/web/teacher_api/auth.py deleted file mode 100644 index 6d21b32..0000000 --- a/BACKEND/src/web/teacher_api/auth.py +++ /dev/null @@ -1,102 +0,0 @@ -from flask import request, make_response, jsonify -from psycopg2.extras import RealDictCursor - -import env -from app import app -from database import get_connection -from decorators import teacher_required -from services import captcha_service -from utils import salt_crypt - - -@app.route("/api/teacher/auth") -def teacher_auth_captcha(): - captcha = captcha_service.generate_with_validation() - return { - "base64string": captcha.base64image, - "validate": captcha.sha256_hash, - } - - -@app.route("/api/teacher/auth", methods=["POST"]) -def teacher_auth_post(): - login, password = request.json["login"], request.json["password"] - - if env.REQUIRE_CAPTCHA: - base64image, captchaUserInput, captchaValidate = ( - request.json["base64image"], - request.json["captchaUserInput"], - request.json["captchaValidate"], - ) - - success_captcha = captcha_service.validate( - base64image, captchaUserInput, captchaValidate - ) - - if not success_captcha: - return make_response({"success": False, "use_redirect": False}, 403) - - connection = get_connection() - cursor = connection.cursor(cursor_factory=RealDictCursor) - - cursor.execute( - f""" - SELECT id from globalusers - WHERE role = 'TEACHER' - AND password = %s - AND login = %s - """, - (password, login), - ) - - resp = make_response({"success": False, "use_redirect": False}, 403) - - if cursor.fetchone(): - resp = make_response({"success": True}) - client_hash = salt_crypt(login, password) - resp.set_cookie("teacher", f"{login}_{password}_{client_hash}") - - return resp - - -@app.route("/api/teacher/changecred", methods=["POST"]) -@teacher_required -def teacher_change_credential_post(): - new_login, new_password, current_password = ( - request.json["login"], - request.json["password"], - request.json["current_password"], - ) - - connection = get_connection() - cursor = connection.cursor(cursor_factory=RealDictCursor) - - cursor.execute( - f""" - SELECT id from globalusers - WHERE role = 'TEACHER' - AND password = %s - AND login = %s - """, - (current_password, new_login), - ) - - resp = make_response({"success": False, "use_redirect": False}, 422) - - user_db = cursor.fetchone() - - print(user_db) - if user_db is None: - pass - elif len(user_db) > 0: - cursor.execute( - "UPDATE globalusers SET login = %s, password = %s WHERE id = %s;", - (new_login, new_password, user_db["id"]), - ) - resp = make_response({"success": True}) - client_hash = salt_crypt(new_login, new_password) - resp.set_cookie("teacher", f"{new_login}_{new_password}_{client_hash}") - - connection.commit() - - return resp diff --git a/BACKEND/src/web/teacher_api/champs.py b/BACKEND/src/web/teacher_api/champs.py deleted file mode 100644 index 9795ed8..0000000 --- a/BACKEND/src/web/teacher_api/champs.py +++ /dev/null @@ -1,121 +0,0 @@ -import re -import string - -from flask import request, redirect - -from app import app -from database import get_connection -from database.createTables import get_query_users_table, get_query_sends_table -from decorators import teacher_required - - -@app.route("/api/teacher/champs") -@teacher_required -def get_champs_route(): - connection = get_connection() - cursor = connection.cursor() - - cursor.execute("SELECT id, name, started FROM champs ") - - champs = cursor.fetchall() - champs = list(map(lambda x: [*x], champs)) - champs = list(map(lambda x: {"id": x[0], "name": x[1], "start_dt": x[2]}, champs)) - - return champs - - -@app.route("/api/teacher/champs", methods=["POST"]) -@teacher_required -def create_champ_post_teacher(): - name = request.json["name"] - - connection = get_connection() - cur = connection.cursor() - - cur.execute("INSERT INTO champs (name) VALUES (%s)", (name,)) - - connection.commit() - - cur.execute("SELECT currval(pg_get_serial_sequence('champs','id'));") - champ_id = cur.fetchone()[0] - - cur.execute(get_query_users_table(champ_id)) - cur.execute(get_query_sends_table(champ_id)) - - connection.commit() - - return redirect("/admin") - - -@app.route("/api/teacher/champs/") -@teacher_required -# @ValidateParameters -def get_champs_byid_route(champ_id): - connection = get_connection() - cur = connection.cursor() - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(champ_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_ids = [] - - sql = "SELECT * FROM problems WHERE id = -1 " - - strs = string.ascii_uppercase - - for task in problems_ids_temp: - if task is not None: - problems_ids.append(task) - sql += "OR id = %s " - - cur.execute(sql, tuple(problems_ids)) - - tasks_dict = dict.fromkeys(problems_ids) - - x = list(cur.fetchall()) - - tasks = [] - - for task in x: - _id = task[0] - name = task[1] - tasks_dict[_id] = task - tasks.append({"letter": strs[problems_ids.index(_id)], "id": _id, "name": name}) - - return {"tasks": tasks, "id": fetch[0], "name": fetch[1]} - - -@app.route("/api/teacher/champs/", methods=["POST"]) -@teacher_required -def settings_post_teacher_api(champ_id): - connection = get_connection() - cur = connection.cursor() - - form = request.json - problem = form["problem"] - problem_id = form["problem_id"] - - cur.execute(f"""SELECT id FROM problems WHERE id=%s""", (problem_id,)) - - prefetched_problem = cur.fetchone() - - print(prefetched_problem) - - if problem_id == "" or prefetched_problem is None: - return {"success": "false"}, 400 - problem_id = int(problem_id) - - print(problem, problem_id) - - problem_validation_result = re.fullmatch("[a-zA-z]", problem) - if not problem_validation_result: - return {"success": "false"}, 400 - - cur.execute( - f"""UPDATE champs SET {problem} = %s WHERE id = %s""", (problem_id, champ_id) - ) - - connection.commit() - - return {"success": "true"} diff --git a/BACKEND/src/web/teacher_api/problems.py b/BACKEND/src/web/teacher_api/problems.py deleted file mode 100644 index d253d92..0000000 --- a/BACKEND/src/web/teacher_api/problems.py +++ /dev/null @@ -1,111 +0,0 @@ -import json - -from flask import request -from psycopg2.extras import RealDictCursor - -from app import app -from database import get_connection -from decorators import teacher_required -from utils import fix_new_line - - -@app.route("/api/teacher/problems") -@teacher_required -def get_problems_api_route(): - connection = get_connection() - cursor = connection.cursor(cursor_factory=RealDictCursor) - - cursor.execute("SELECT id, name, description FROM problems") - problems = cursor.fetchall() - - return problems - - -@app.route("/api/teacher/problems/") -@teacher_required -def get_problems_byid_api_route(problem_id): - connection = get_connection() - cursor = connection.cursor(cursor_factory=RealDictCursor) - - cursor.execute( - f""" - SELECT id, name, description, "in", "out", examples, tests - FROM problems WHERE id = %s - """, - (problem_id,), - ) - problem = cursor.fetchone() - - print(problem) - - if problem is None: - return "", 404 - - out_data = { - "tests": [], - "examples": fix_new_line(json.loads(problem["examples"])), - "name": problem["name"], - "description": problem["description"], - "out_data": problem["out"], - "in_data": problem["in"], - } - - return out_data - - -@app.route("/api/teacher/problems/add", methods=["POST"]) -@teacher_required -def teacher_list_problems_add(): - connection = get_connection() - cursor = connection.cursor() - - build = request.json["build"] - print(build) - - try: - build_json = json.loads(build) - - problem_type = build_json.get("type", "question") - except Exception as e: - print("Maybe Json parse exception \n" + str(e)) - return "Not json (404 ERR)", 400 - - print() - - if problem_type == "quiz": - print() - - cursor.execute( - """ - INSERT INTO problems (name, description, "in", out, examples, tests, is_question) - VALUES (%s, %s, %s, %s, %s, %s, TRUE) - """, - (build_json["name"], "Тест", "-", "-", "[]", json.dumps(build_json)), - ) - - pass - elif problem_type == "question": - build_json["tests"] = json.dumps(build_json["tests"]) - build_json["examples"] = json.dumps(build_json["examples"]) - - print(build) - print(build_json) - - cursor.execute( - """ - INSERT INTO problems (name, description, "in", out, examples, tests) - VALUES (%s, %s, %s, %s, %s, %s) - """, - ( - build_json["name"], - build_json["description"], - build_json["in"], - build_json["out"], - build_json["examples"], - build_json["tests"], - ), - ) - - connection.commit() - - return {"success": True} diff --git a/BACKEND/src/web/teacher_api/students.py b/BACKEND/src/web/teacher_api/students.py deleted file mode 100644 index 752e977..0000000 --- a/BACKEND/src/web/teacher_api/students.py +++ /dev/null @@ -1,47 +0,0 @@ -import random - -from flask import request -from psycopg2.extras import RealDictCursor - -from app import app -from database import get_connection -from decorators import teacher_required - -from decorators import redis_conn - - -@app.route("/api/teacher/champs//users") -@teacher_required -def get_users_get_route(champ_id): - connection = get_connection() - cur = connection.cursor(cursor_factory=RealDictCursor) - - cur.execute(f"SELECT name, login, password FROM champUsers_{champ_id}") - users = cur.fetchall() - - return users - - -@app.route("/api/teacher/champs//add_users", methods=["POST"]) -@teacher_required -@redis_conn -def create_users_in_champ_post_teachers_api(champ_id, r): - users = request.json["users"].replace("\r", "").split("\n") - - connection = get_connection() - cursor = connection.cursor() - - for name in users: - login = "".join(map(str, [random.randint(0, 9) for _ in range(5)])) - password = "".join(map(str, [random.randint(0, 9) for _ in range(5)])) - - cursor.execute( - f"INSERT INTO champUsers_{champ_id} (login, password, name) VALUES (%s, %s, %s)", - (login, password, name), - ) - - connection.commit() - - r.delete(f"r-champ-{champ_id}-stats") - - return {"success": "true"}, 201 diff --git a/BACKEND/src/web/teacher_api/students_sends.py b/BACKEND/src/web/teacher_api/students_sends.py deleted file mode 100644 index a167c0e..0000000 --- a/BACKEND/src/web/teacher_api/students_sends.py +++ /dev/null @@ -1,138 +0,0 @@ -import json -import string - -import psycopg2 -from flask import request, abort -from psycopg2.extras import RealDictCursor - -from app import app -from database import get_connection -from decorators import redis_conn -from web.api.battle import JSON_MIMETYPE - - -@app.route("/api/teacher/champs//stats") -@redis_conn -def get_stats_teacher_api(champ_id, r): - redis_cache = r.get(f"r-champ-{champ_id}-stats") - if redis_cache is not None: - response = app.response_class( - response=redis_cache, - status=200, - mimetype=JSON_MIMETYPE, - ) - print("redis") - return response - - connection = get_connection() - cur = connection.cursor() - - cur.execute("SELECT * FROM champs WHERE id = %s", (str(champ_id),)) - - fetch = cur.fetchone() # Can be None - problems_ids_temp = fetch[3:] - problems_counts = 0 - - for i in problems_ids_temp: - if i is not None: - problems_counts += 1 - else: - break - - strs = string.ascii_uppercase - - print(problems_counts) - - cur.execute( - f""" - SELECT u.*, MAX(s.send_time) AS send_time - FROM champusers_{champ_id} u - LEFT JOIN champsends_{champ_id} s ON u.id = s.user_id - GROUP BY u.id - ORDER BY score DESC, send_time ASC; - """ - ) - - fetch = cur.fetchall() # Can be None - - users = [] - - for i, usr in enumerate(fetch): - user_id = usr[0] - score = usr[15] - last_send = usr[16] - last_send = ( - None if last_send is None else last_send.strftime("%m/%d/%Y, %H:%M:%S") - ) - - nickname = usr[3] - problems_score = usr[4 : problems_counts + 4] - - # problems_score = list(map(lambda s: (s, "")[s is None], problems_score)) - - users.append( - { - "position": i + 1, - "name": nickname, - "user_id": user_id, - "score": score, - "problems_score": problems_score, - "last_send": last_send, - } - ) - - print() - - resp_string = {"success": True, "cols": strs[:problems_counts], "users": users} - - r.set(f"r-champ-{champ_id}-stats", json.dumps(resp_string)) - return resp_string - - -@app.route("/api/teacher/champs//stats/search") -def get_stats_teacher_api_get_by_task_and_user(champ_id): - args = request.args - user_id, problem_letter = args["user_id"], args["problem"] - - conn = get_connection() - cursor = conn.cursor(cursor_factory=RealDictCursor) - - try: - cursor.execute( - f""" - SELECT * - FROM champsends_{champ_id} - WHERE problem_letter = %s - AND user_id = %s - ORDER BY score DESC, send_time DESC - LIMIT 1 - - """, - (problem_letter, user_id), - ) - except psycopg2.errors.UndefinedTable: - abort(404) - - res = cursor.fetchone() - - if res is None: - abort(404) - - tests = [] - result = [] - try: - result = json.loads(res["description"]) - except TypeError: - pass - - for i, test in enumerate(result): - message = test["msg"] - out = test["out"] - - if message == "WRONG_ANSWER": - out = """ВЫВОД СКРЫТ""" - - to_add = {"id": i + 1, "time": test["time"], "msg": message, "out": out} - tests.append(to_add) - - return {**{"tests": tests}, **res} diff --git a/BACKEND/src/web/validation_form/__init__.py b/BACKEND/src/web/validation_form/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/BACKEND/src/web/validation_form/admin.py b/BACKEND/src/web/validation_form/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/BACKEND/src/web/validation_form/api.py b/BACKEND/src/web/validation_form/api.py deleted file mode 100644 index 24f5ffb..0000000 --- a/BACKEND/src/web/validation_form/api.py +++ /dev/null @@ -1,16 +0,0 @@ -from wtforms import Form, StringField, IntegerField -from wtforms.validators import DataRequired, Regexp - -from utils import LETTER_REGEX - - -class LoginForm(Form): - id = IntegerField("id", validators=[DataRequired()]) - login = StringField("login", validators=[DataRequired()]) - password = StringField("password", validators=[DataRequired()]) - - -class SendProgramForm(Form): - cars = StringField("cars", validators=[DataRequired()]) - src = StringField("src", validators=[DataRequired()]) - problem = StringField("problem", validators=[DataRequired(), Regexp(LETTER_REGEX)]) diff --git a/BACKEND/src/web/validation_form/solution_processing.py b/BACKEND/src/web/validation_form/solution_processing.py deleted file mode 100644 index e69de29..0000000 diff --git a/BACKEND/src/web/validation_form/teacher_api.py b/BACKEND/src/web/validation_form/teacher_api.py deleted file mode 100644 index e69de29..0000000 diff --git a/BACKEND/src/wsgi.py b/BACKEND/src/wsgi.py deleted file mode 100644 index 033109f..0000000 --- a/BACKEND/src/wsgi.py +++ /dev/null @@ -1,3 +0,0 @@ -from main import webapp - -app = webapp() diff --git a/BACKEND_V2/.gitattributes b/BACKEND_V2/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/BACKEND_V2/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/BACKEND_V2/.gitignore b/BACKEND_V2/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/BACKEND_V2/.gitignore @@ -0,0 +1,40 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Kotlin ### +.kotlin diff --git a/BACKEND_V2/Dockerfile b/BACKEND_V2/Dockerfile new file mode 100644 index 0000000..5231258 --- /dev/null +++ b/BACKEND_V2/Dockerfile @@ -0,0 +1,10 @@ +FROM openjdk:17-jdk-slim AS build +WORKDIR /app +COPY . . +RUN ./gradlew build -x test + +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY --from=build /app/build/libs/*.jar app.jar +EXPOSE 8080 +CMD ["java", "-jar", "app.jar"] diff --git a/BACKEND_V2/build.gradle.kts b/BACKEND_V2/build.gradle.kts new file mode 100644 index 0000000..d1d0280 --- /dev/null +++ b/BACKEND_V2/build.gradle.kts @@ -0,0 +1,81 @@ +plugins { + kotlin("jvm") version "1.9.25" + kotlin("plugin.spring") version "1.9.25" + id("org.springframework.boot") version "3.4.1" + id("io.spring.dependency-management") version "1.1.7" + kotlin("plugin.jpa") version "1.9.25" + id("org.jetbrains.kotlin.kapt") version "1.9.25" +} + +group = "ru.codebattles" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +dependencies { + + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.springdoc:springdoc-openapi-starter-common:2.8.8") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8") + implementation("org.flywaydb:flyway-core:11.8.2") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + compileOnly("org.projectlombok:lombok") + runtimeOnly("org.postgresql:postgresql") + // https://mvnrepository.com/artifact/org.flywaydb/flyway-database-postgresql + runtimeOnly("org.flywaydb:flyway-database-postgresql:11.8.1") + annotationProcessor("org.projectlombok:lombok") + + implementation("org.springframework.boot:spring-boot-starter-actuator") + + implementation("org.mapstruct:mapstruct:1.5.5.Final") + kapt("org.mapstruct:mapstruct-processor:1.5.5.Final") + + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + implementation("io.jsonwebtoken:jjwt-impl:0.11.5") + implementation("io.jsonwebtoken:jjwt-jackson:0.11.5") +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} + +allOpen { + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Embeddable") +} + +tasks.withType { + useJUnitPlatform() +} + +kapt { + correctErrorTypes = true + arguments { + arg("mapstruct.unmappedTargetPolicy", "ignore") + } +} + +sourceSets { + main { + java { + srcDir("build/generated/sources/annotationProcessor/java/main") + } + } +} \ No newline at end of file diff --git a/BACKEND_V2/gradle/wrapper/gradle-wrapper.jar b/BACKEND_V2/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/BACKEND_V2/gradle/wrapper/gradle-wrapper.jar differ diff --git a/BACKEND_V2/gradle/wrapper/gradle-wrapper.properties b/BACKEND_V2/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e2847c8 --- /dev/null +++ b/BACKEND_V2/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/BACKEND_V2/gradlew b/BACKEND_V2/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/BACKEND_V2/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/BACKEND_V2/gradlew.bat b/BACKEND_V2/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/BACKEND_V2/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/BACKEND_V2/settings.gradle.kts b/BACKEND_V2/settings.gradle.kts new file mode 100644 index 0000000..4189a93 --- /dev/null +++ b/BACKEND_V2/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "backend" +gradle.startParameter.warningMode = WarningMode.None + diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/BackendV2Application.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/BackendV2Application.kt new file mode 100644 index 0000000..623d2d3 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/BackendV2Application.kt @@ -0,0 +1,14 @@ +package ru.codebattles.backend + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication +import ru.codebattles.backend.core.properties.JwtTokenProperties + +@SpringBootApplication +@EnableConfigurationProperties(JwtTokenProperties::class) +class BackendV2Application + +fun main(args: Array) { + runApplication(*args) +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/Checked.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/Checked.kt new file mode 100644 index 0000000..bc4481c --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/Checked.kt @@ -0,0 +1,6 @@ +package ru.codebattles.backend.annotations + +/* +* Annotation for mark method as checked for security + */ +annotation class Checked(val comment: String = "") diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/CompetitionAccessRequired.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/CompetitionAccessRequired.kt new file mode 100644 index 0000000..54c1807 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/CompetitionAccessRequired.kt @@ -0,0 +1,5 @@ +package ru.codebattles.backend.annotations + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class CompetitionAccessRequired diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/CompetitionId.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/CompetitionId.kt new file mode 100644 index 0000000..2618996 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/annotations/CompetitionId.kt @@ -0,0 +1,5 @@ +package ru.codebattles.backend.annotations + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class CompetitionId \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/aspects/CompetitionAccessAspect.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/aspects/CompetitionAccessAspect.kt new file mode 100644 index 0000000..a9e8cb8 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/aspects/CompetitionAccessAspect.kt @@ -0,0 +1,46 @@ +package ru.codebattles.backend.aspects + +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.aspectj.lang.reflect.MethodSignature +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import ru.codebattles.backend.annotations.CompetitionId +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.services.CompetitionService + +@Aspect +@Component +class CompetitionAccessAspect( + private val competitionService: CompetitionService, +) { + @Before("@annotation(ru.codebattles.backend.annotations.CompetitionAccessRequired)") + fun checkAccess(joinPoint: JoinPoint) { + val authentication = SecurityContextHolder.getContext().authentication + if (authentication == null || !authentication.isAuthenticated) { + throw IllegalStateException("User is not authenticated!") + } + val user: User = authentication.principal as User + + + val method = (joinPoint.signature as MethodSignature).method + val parameterAnnotations = method.parameterAnnotations + val args = joinPoint.args + + for ((index, annotations) in parameterAnnotations.withIndex()) { + if (annotations.any { it is CompetitionId }) { + val competitionId = args[index] as? Long + ?: throw IllegalArgumentException("Invalid competition ID") + + if (!competitionService.checkAccessForCompetitionByUser(user, competitionId)) { + throw AccessDeniedException("No access to competition $competitionId") + } + return + } + } + + throw IllegalStateException("No parameter annotated with @CompetitionId found") + } +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/commandrunner/DefaultAdminCommandRunner.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/commandrunner/DefaultAdminCommandRunner.kt new file mode 100644 index 0000000..208a49b --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/commandrunner/DefaultAdminCommandRunner.kt @@ -0,0 +1,39 @@ +package ru.codebattles.backend.core.commandrunner + +import org.springframework.boot.CommandLineRunner +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Component +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.entity.UserRole +import ru.codebattles.backend.entity.Variable +import ru.codebattles.backend.repository.UserRepository +import ru.codebattles.backend.repository.VariablesRepository + +@Component +class DefaultAdminCommandRunner( + val variablesRepository: VariablesRepository, + val userRepository: UserRepository, + val passwordEncoder: PasswordEncoder, +) : CommandLineRunner { + + val VARIABLE_KEY: String = "DEFAULT_USER_EXECUTOR_COMPLETE" + + + override fun run(vararg args: String?) { + val notFirstExecute = variablesRepository.existsByKey(VARIABLE_KEY) + val userWithUsernameAdminExists = userRepository.existsByMusername("admin") + + if (notFirstExecute) { + return + } + + if (!userWithUsernameAdminExists) { + val user = User(mpassword = passwordEncoder.encode("admin"), musername = "admin") + user.roles = mutableSetOf(UserRole.ROLE_ADMIN, UserRole.USER) + + userRepository.save(user) + } + + variablesRepository.save(Variable(key = VARIABLE_KEY, value = "true")) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/commandrunner/DefaultCheckerCommandRunner.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/commandrunner/DefaultCheckerCommandRunner.kt new file mode 100644 index 0000000..94213b0 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/commandrunner/DefaultCheckerCommandRunner.kt @@ -0,0 +1,32 @@ +package ru.codebattles.backend.core.commandrunner + +import org.springframework.boot.CommandLineRunner +import org.springframework.stereotype.Component +import ru.codebattles.backend.entity.Checker +import ru.codebattles.backend.entity.Variable +import ru.codebattles.backend.repository.CheckerRepository +import ru.codebattles.backend.repository.VariablesRepository + +@Component +class DefaultCheckerCommandRunner( + val variablesRepository: VariablesRepository, + private val checkerRepository: CheckerRepository, +) : CommandLineRunner { + + val VARIABLE_KEY: String = "DEFAULT_CHECKER_EXECUTOR_COMPLETE" + + + override fun run(vararg args: String?) { + val notFirstExecute = variablesRepository.existsByKey(VARIABLE_KEY) + if (notFirstExecute) return + + val checker = Checker( + displayName = "Default Python3 Checker", + languageHighlightName = "python", + address = "http://checker-python:7070/api/v1/test" + ) + + checkerRepository.save(checker) + variablesRepository.save(Variable(key = VARIABLE_KEY, value = "true")) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/OpenApiConfig.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/OpenApiConfig.kt new file mode 100644 index 0000000..e3954d3 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/OpenApiConfig.kt @@ -0,0 +1,30 @@ +package ru.codebattles.backend.core.config + +import io.swagger.v3.oas.annotations.OpenAPIDefinition +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType +import io.swagger.v3.oas.annotations.info.Contact +import io.swagger.v3.oas.annotations.info.Info +import io.swagger.v3.oas.annotations.security.SecurityScheme + +@OpenAPIDefinition( + info = Info( + title = "Codebattles backend", + description = """ + This is a backend for the Codebattles competition system. + It is designed to handle various aspects of the competition, including user management, problem management, and competition management. + The system allows users to register, login, and participate in competitions. + + Tips: + - [ADMIN] - Requires admin role + """, + version = "0.1.0", + contact = Contact(name = "Suslov Yaroslav", email = "genius@doctorixx.ru") + ) +) +@SecurityScheme( + name = "JWT", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + scheme = "bearer" +) +class OpenApiConfig diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/PasswordEncoderConfig.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/PasswordEncoderConfig.kt new file mode 100644 index 0000000..0ba7e57 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/PasswordEncoderConfig.kt @@ -0,0 +1,15 @@ +package ru.codebattles.backend.core.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder + +@Configuration +class PasswordEncoderConfig { + @Bean + fun encoder(): PasswordEncoder { + return BCryptPasswordEncoder() + } + +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/SecurityConfig.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/SecurityConfig.kt new file mode 100644 index 0000000..b6bcb3d --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/config/SecurityConfig.kt @@ -0,0 +1,79 @@ +package ru.codebattles.backend.core.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpStatus +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.HttpStatusEntryPoint +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import ru.codebattles.backend.core.filter.JwtAuthenticationFilter + + +@Configuration +@EnableMethodSecurity(jsr250Enabled = true) +class SecurityConfig( + private val jwtAuthenticationFilter: JwtAuthenticationFilter, +) { + + @Bean + fun authenticationManager(authConfig: AuthenticationConfiguration): AuthenticationManager { + return authConfig.authenticationManager + } + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf().disable() + .cors { cors -> + cors.configurationSource { request -> + CorsConfiguration().applyPermitDefaultValues().also { + it.allowedOriginPatterns = listOf("*") + it.allowedMethods = listOf("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + it.allowedHeaders = listOf("*") + it.allowCredentials = true + } + } + } + .authorizeRequests() +// .requestMatchers("/api/auth/**").permitAll() + .requestMatchers( + "/api/ping", + "/api/auth/login", + "/api/auth/register", + "/api/check_system_callback/**", + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-ui.html", + "/favicon.ico", + "/webjars/**" + ).permitAll() + .requestMatchers( + "/api/**" + ).authenticated() +// .anyRequest().permitAll() + .and() + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) + .exceptionHandling() + .authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + return http.build() + } + + @Bean + fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = listOf("http://localhost:5173") + configuration.allowedMethods = listOf("*") + configuration.allowedHeaders = listOf("*") + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source + } + +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/filter/JwtAuthenticationFilter.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/filter/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..a46e013 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/filter/JwtAuthenticationFilter.kt @@ -0,0 +1,38 @@ +package ru.codebattles.backend.core.filter + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import ru.codebattles.backend.services.JwtService +import ru.codebattles.backend.services.UserService + +@Component +class JwtAuthenticationFilter( + private val jwtService: JwtService, + val userService: UserService +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val authHeader = request.getHeader("Authorization") + if (authHeader != null && authHeader.startsWith("Bearer ")) { + val token = authHeader.substring(7) + if (jwtService.validateToken(token)) { + val username = jwtService.getUsernameFromToken(token) + val user = userService.getByUsername(username) + val authentication = UsernamePasswordAuthenticationToken(user, null, user.authorities) + authentication.details = WebAuthenticationDetailsSource().buildDetails(request) + SecurityContextHolder.getContext().authentication = authentication + } + } + filterChain.doFilter(request, response) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/properties/JwtTokenProperties.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/properties/JwtTokenProperties.kt new file mode 100644 index 0000000..8dba134 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/core/properties/JwtTokenProperties.kt @@ -0,0 +1,8 @@ +package ru.codebattles.backend.core.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("codebattles.jwt") +data class JwtTokenProperties ( + val secretKey: String?, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/AnswerDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/AnswerDto.kt new file mode 100644 index 0000000..467cb2b --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/AnswerDto.kt @@ -0,0 +1,38 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema +import ru.codebattles.backend.entity.AnswerStatus +import java.util.* + +data class AnswerDto( + @Schema(description = "Unique identifier of the answer", example = "1") + val id: Long? = null, + + @Schema(description = "User who submitted the answer") + val user: UserDto, + + @Schema( + description = "Current status of the answer", + example = "IN_PROGRESS", + ) + val status: AnswerStatus = AnswerStatus.IN_PROGRESS, + + @Schema(description = "Score awarded for the answer", example = "100") + val score: Int? = null, + + @Schema(description = "Code submitted by the user", example = "print('Hello, World!')") + val code: String, + + @Schema(description = "Result of the code execution", example = "Success") + val result: String? = null, + + @Schema(description = "Checker used to evaluate the answer") + val checker: CheckerDto, + + @Schema(description = "Timestamp when the answer was created", example = "2023-01-01T12:00:00Z") + val createdAt: Date, + + @Schema(description = "Details of the competition problem associated with the answer") + val competitionsProblems: CompetitionsProblemsDto? = null +) + diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CheckerDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CheckerDto.kt new file mode 100644 index 0000000..5e59509 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CheckerDto.kt @@ -0,0 +1,14 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class CheckerDto( + @Schema(description = "Unique identifier of the checker", example = "1") + val id: Long? = null, + + @Schema(description = "Display name of the checker", example = "Default Python3 Checker") + val displayName: String, + + @Schema(description = "Programming language used by the checker", example = "python") + val languageHighlightName: String, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionCreateDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionCreateDto.kt new file mode 100644 index 0000000..a893c78 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionCreateDto.kt @@ -0,0 +1,18 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class CompetitionCreateDto( + @Schema(description = "Name of the competition", example = "Code Battles 2023") + val name: String, + + @Schema(description = "Description of the competition", example = "A competitive coding event") + val description: String, + + @Schema(description = "Start time of the competition", example = "2023-01-01T10:00:00") + val startedAt: LocalDateTime? = null, + + @Schema(description = "End time of the competition", example = "2023-01-01T18:00:00") + val endedAt: LocalDateTime? = null +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionDto.kt new file mode 100644 index 0000000..01e6328 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionDto.kt @@ -0,0 +1,24 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class CompetitionDto( + @Schema(description = "Unique identifier of the competition", example = "1") + val id: Long, + + @Schema(description = "Set of checkers associated with the competition") + val checkers: Set? = emptySet(), + + @Schema(description = "Name of the competition", example = "Code Battles 2023") + val name: String, + + @Schema(description = "Description of the competition", example = "A competitive coding event") + val description: String, + + @Schema(description = "Start time of the competition", example = "2023-01-01T10:00:00") + val startedAt: LocalDateTime? = null, + + @Schema(description = "End time of the competition", example = "2023-01-01T18:00:00") + val endedAt: LocalDateTime? = null +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionsProblemsDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionsProblemsDto.kt new file mode 100644 index 0000000..1962585 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CompetitionsProblemsDto.kt @@ -0,0 +1,17 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class CompetitionsProblemsDto( + @Schema(description = "Unique identifier of the competition problem", example = "1") + val id: Long, + + @Schema(description = "Priority of problem for sorting", example = "12") + val priority: Int, + + @Schema(description = "Unique identifier of the competition problem") + val problem: ProblemDto, + + @Schema(description = "Unique identifier of the competition problem") + val slug: String, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreateProblemDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreateProblemDto.kt new file mode 100644 index 0000000..405ffa6 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreateProblemDto.kt @@ -0,0 +1,23 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class CreateProblemDto( + @Schema(description = "Name of the problem", example = "Sum of Two Numbers") + val name: String, + + @Schema(description = "Description of the problem", example = "Calculate the sum of two integers.") + val description: String, + + @Schema(description = "Input data for the problem", example = "1 2") + val inData: String, + + @Schema(description = "Expected output data for the problem", example = "3") + val outData: String, + + @Schema(description = "Test cases for the problem (JSON)", example = """{"in":"1", "out":"3"}""") + val tests: String, + + @Schema(description = "Example cases for the problem (JSON)", example = """{"in":"1", "out":"3"}""") + val examples: String +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreateUserDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreateUserDto.kt new file mode 100644 index 0000000..2bc54bf --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreateUserDto.kt @@ -0,0 +1,18 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull + +data class CreateUserDto( + @field:NotNull @field:NotEmpty + @Schema(description = "Username of the user", example = "john_doe") + val musername: String? = null, + + @field:NotNull @field:NotEmpty + @Schema(description = "Password of the user", example = "securepassword123") + val mpassword: String? = null, + + @Schema(description = "Full name of the user", example = "John Doe") + val name: String? = "" +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/ProblemDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/ProblemDto.kt new file mode 100644 index 0000000..db7d2b3 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/ProblemDto.kt @@ -0,0 +1,26 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class ProblemDto( + @Schema(description = "Unique identifier of the problem", example = "1") + val id: Long, + + @Schema(description = "Name of the problem", example = "Sum of Two Numbers") + val name: String, + + @Schema(description = "Description of the problem", example = "Calculate the sum of two integers.") + val description: String, + + @Schema(description = "Input data for the problem", example = "1 2") + val inData: String, + + @Schema(description = "Expected output data for the problem", example = "3") + val outData: String, + + @Schema(description = "Example cases for the problem", example = "Input: 1 2, Output: 3") + val examples: String, + + @Schema(description = "Indicates if the problem is public", example = "true") + val public: Boolean? = false +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserDto.kt new file mode 100644 index 0000000..3b51de4 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserDto.kt @@ -0,0 +1,12 @@ +package ru.codebattles.backend.dto + + +import io.swagger.v3.oas.annotations.media.Schema + +data class UserDto( + @Schema(description = "Unique identifier of the user", example = "1") + val id: Long, + + @Schema(description = "Username of the user", example = "john_doe") + val username: String, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserProfileDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserProfileDto.kt new file mode 100644 index 0000000..fee32a7 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserProfileDto.kt @@ -0,0 +1,8 @@ +package ru.codebattles.backend.dto + +data class UserProfileDto( + val id: Long, + val username: String, + val name: String, + val roles: List, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserProfileEditDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserProfileEditDto.kt new file mode 100644 index 0000000..fb07331 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/UserProfileEditDto.kt @@ -0,0 +1,5 @@ +package ru.codebattles.backend.dto + +data class UserProfileEditDto( + val name: String +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/AnswerMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/AnswerMapper.kt new file mode 100644 index 0000000..267d52c --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/AnswerMapper.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.Mapper +import ru.codebattles.backend.dto.AnswerDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.Answer + +@Mapper(componentModel = "spring") +interface AnswerMapper : AbstractMapper diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CheckerMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CheckerMapper.kt new file mode 100644 index 0000000..98a802f --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CheckerMapper.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.Mapper +import ru.codebattles.backend.dto.CheckerDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.Checker + +@Mapper(componentModel = "spring") +interface CheckerMapper : AbstractMapper diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CompetitionsMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CompetitionsMapper.kt new file mode 100644 index 0000000..c93205c --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CompetitionsMapper.kt @@ -0,0 +1,16 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.BeanMapping +import org.mapstruct.Mapper +import org.mapstruct.MappingTarget +import org.mapstruct.NullValuePropertyMappingStrategy +import ru.codebattles.backend.entity.Competition +import ru.codebattles.backend.dto.CompetitionDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.web.entity.CompetitionEditDto + +@Mapper(componentModel = "spring") +interface CompetitionsMapper : AbstractMapper { + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + fun update(dto: CompetitionEditDto?, @MappingTarget entity: Competition?) +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CompetitionsProblemsMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CompetitionsProblemsMapper.kt new file mode 100644 index 0000000..f7db312 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CompetitionsProblemsMapper.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.Mapper +import ru.codebattles.backend.dto.CompetitionsProblemsDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.CompetitionsProblems + +@Mapper(componentModel = "spring") +interface CompetitionsProblemsMapper : AbstractMapper diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CreateProblemsMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CreateProblemsMapper.kt new file mode 100644 index 0000000..4794ce1 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CreateProblemsMapper.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.Mapper +import ru.codebattles.backend.dto.CreateProblemDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.Problem + +@Mapper(componentModel = "spring") +interface CreateProblemsMapper : AbstractMapper diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/ProblemsMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/ProblemsMapper.kt new file mode 100644 index 0000000..bcebcc4 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/ProblemsMapper.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.Mapper +import ru.codebattles.backend.dto.ProblemDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.Problem + +@Mapper(componentModel = "spring") +interface ProblemsMapper : AbstractMapper diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/UserMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/UserMapper.kt new file mode 100644 index 0000000..212e570 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/UserMapper.kt @@ -0,0 +1,17 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.BeanMapping +import org.mapstruct.Mapper +import org.mapstruct.MappingTarget +import org.mapstruct.NullValuePropertyMappingStrategy +import ru.codebattles.backend.dto.UserDto +import ru.codebattles.backend.dto.UserProfileEditDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.User + + +@Mapper(componentModel = "spring") +interface UserMapper : AbstractMapper { + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + fun updateUserProfile(dto: UserProfileEditDto?, @MappingTarget entity: User?) +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/UserProfileMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/UserProfileMapper.kt new file mode 100644 index 0000000..af4e00a --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/UserProfileMapper.kt @@ -0,0 +1,10 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.Mapper +import ru.codebattles.backend.dto.UserProfileDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.User + + +@Mapper(componentModel = "spring") +interface UserProfileMapper : AbstractMapper \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/core/AbstractMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/core/AbstractMapper.kt new file mode 100644 index 0000000..80c043a --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/core/AbstractMapper.kt @@ -0,0 +1,10 @@ +package ru.codebattles.backend.dto.mapper.core + + +interface AbstractMapper { + fun toDto(obj: OBJ): DTO + fun fromDto(obj: DTO): OBJ + fun fromDtoS(obj: List): List + fun toDtoS(obj: List): List + fun toDtoS(obj: Set): List +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Answer.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Answer.kt new file mode 100644 index 0000000..406c060 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Answer.kt @@ -0,0 +1,32 @@ +package ru.codebattles.backend.entity + + +import jakarta.persistence.* + +enum class AnswerStatus { + IN_PROGRESS, + COMPLETED, +} + +@Entity +data class Answer( + @ManyToOne + val competition: Competition, + @ManyToOne + val user: User, + @Enumerated(EnumType.STRING) + var status: AnswerStatus = AnswerStatus.IN_PROGRESS, + var score: Int? = null, + + val code: String, + + @ManyToOne + val checker: Checker, + + var result: String? = null, + + @ManyToOne + val competitionsProblems: CompetitionsProblems? = null + + ) : BaseEntity() + diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/BaseEntity.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/BaseEntity.kt new file mode 100644 index 0000000..8536ee2 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/BaseEntity.kt @@ -0,0 +1,42 @@ +package ru.codebattles.backend.entity + +import jakarta.persistence.* +import lombok.Getter +import lombok.Setter +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.UpdateTimestamp +import java.io.Serializable +import java.util.* + +@Getter +@Setter +@MappedSuperclass +abstract class BaseEntity : Serializable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null + + + @CreationTimestamp + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_at") + val createdAt: Date? = null + + @UpdateTimestamp + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "updated_at") + val updatedAt: Date? = null + + + override fun hashCode(): Int { + return Objects.hash(id) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BaseEntity + return id == other.id + } +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Checker.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Checker.kt new file mode 100644 index 0000000..2492749 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Checker.kt @@ -0,0 +1,10 @@ +package ru.codebattles.backend.entity + +import jakarta.persistence.Entity + +@Entity +data class Checker( + val displayName: String, + val languageHighlightName: String, + val address: String, +) : BaseEntity() diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Competition.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Competition.kt new file mode 100644 index 0000000..2802746 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Competition.kt @@ -0,0 +1,39 @@ +package ru.codebattles.backend.entity + +import jakarta.persistence.* +import java.util.* + +@Entity +@Table(name = "competitions") +data class Competition( + @ManyToMany + var members: MutableSet? = mutableSetOf(), + + @ManyToMany + var checkers: MutableSet? = mutableSetOf(), + + @ManyToOne + var organizer: User?, + + @Column(nullable = false) + var name: String, + + @Column(nullable = false, length = 1000) + var description: String, + + @Column(name = "started_at") + var startedAt: Date? = null, + + @Column(name = "ended_at") + var endedAt: Date? = null, + + @Column(name = "show_rating", nullable = false) + var showRating: Boolean = true, + + @Column(name = "show_output", nullable = false) + var showOutput: Boolean = true, + + @Column(name = "show_input", nullable = false) + var showInput: Boolean = true, + + ) : BaseEntity() diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/CompetitionsProblems.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/CompetitionsProblems.kt new file mode 100644 index 0000000..db26bcd --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/CompetitionsProblems.kt @@ -0,0 +1,16 @@ +package ru.codebattles.backend.entity + +import jakarta.persistence.Entity +import jakarta.persistence.ManyToOne + +@Entity +data class CompetitionsProblems( + val priority: Int, + + val slug: String, + + @ManyToOne + val competition: Competition, + @ManyToOne + val problem: Problem, +) : BaseEntity() \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/LeaderBoardAllTasksQuery.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/LeaderBoardAllTasksQuery.kt new file mode 100644 index 0000000..e76b762 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/LeaderBoardAllTasksQuery.kt @@ -0,0 +1,23 @@ +package ru.codebattles.backend.entity + +import java.util.* + +data class LeaderBoardAllTasksQuery( + val userId: Long, + val competitionproblemId: Long, + val maxScore: Long + +) + +data class LeaderBoardScoreOrderQuery( + val userId: Long, + val userX: String, + val score: Long, + val time: Date + +) + +data class Leaderboard( + val score: List, + val data: Map> +) diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Problem.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Problem.kt new file mode 100644 index 0000000..a71ac30 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Problem.kt @@ -0,0 +1,27 @@ +package ru.codebattles.backend.entity + +import jakarta.persistence.* + + +@Entity +data class Problem( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + val name: String, + + @Column(name = "description", columnDefinition = "TEXT") + val description: String, + + @Column(name = "in_data", columnDefinition = "TEXT") + val inData: String, + + @Column(name = "out_data", columnDefinition = "TEXT") + val outData: String, + + @Column(name = "tests", columnDefinition = "TEXT") + val tests: String, + + @Column(name = "examples", columnDefinition = "TEXT") + val examples: String +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/User.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/User.kt new file mode 100644 index 0000000..eddc589 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/User.kt @@ -0,0 +1,51 @@ +package ru.codebattles.backend.entity + + +import jakarta.persistence.* +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +@Entity +@Table(name = "users_") +data class User( + @Column(name = "username") + var musername: String?, + @Column(name = "password") + var mpassword: String?, + + var name: String? = "", + + @ElementCollection(targetClass = UserRole::class, fetch = FetchType.EAGER) + @CollectionTable( + name = "users_roles", + joinColumns = [JoinColumn(name = "user_id")] + ) + @Enumerated(EnumType.STRING) + @Column(name = "role") + var roles: MutableSet = mutableSetOf(), + + ) : UserDetails, BaseEntity() { + override fun getAuthorities(): MutableCollection { + return roles.map { SimpleGrantedAuthority(it.name) }.toMutableList() + } + + override fun getPassword() = mpassword + override fun getUsername() = musername + + + fun isAdmin(): Boolean { + return authorities.contains(SimpleGrantedAuthority("ROLE_ADMIN")) || authorities.contains(SimpleGrantedAuthority("ADMIN")) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is User) return false + return this.id == other.id + } + + override fun hashCode(): Int { + return id?.hashCode() ?: 0 + } + +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/UserRole.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/UserRole.kt new file mode 100644 index 0000000..86028b4 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/UserRole.kt @@ -0,0 +1,8 @@ +package ru.codebattles.backend.entity + +enum class UserRole { + USER, + ROLE_ADMIN, + TESTTTSTCBEUYBEYUBYUCYUBYUBUBY, + SYSADMIN +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Variable.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Variable.kt new file mode 100644 index 0000000..491d132 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Variable.kt @@ -0,0 +1,17 @@ +package ru.codebattles.backend.entity + + +import jakarta.persistence.* + +@Entity +@Table(name = "variables") +data class Variable( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @Column(name = "name", unique = true, nullable = false) + val key: String, + + @Column(name = "value", nullable = true) + val value: String?, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/AnswerRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/AnswerRepository.kt new file mode 100644 index 0000000..546431c --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/AnswerRepository.kt @@ -0,0 +1,12 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.codebattles.backend.entity.Answer +import ru.codebattles.backend.entity.User + +interface AnswerRepository : JpaRepository { + fun getAllByUserAndCompetitionId(user: User, compId: Long): List + fun getAllByUserIdAndCompetitionId(userId: Long, compId: Long): List + fun getFirstByUserIdAndCompetitionsProblemsIdOrderByCreatedAtDesc(userId: Long, compProbId: Long): Answer + +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CheckerRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CheckerRepository.kt new file mode 100644 index 0000000..8602491 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CheckerRepository.kt @@ -0,0 +1,7 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.codebattles.backend.entity.Checker + +interface CheckerRepository : JpaRepository { + fun findByIdIn(id: Set): MutableSet} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CompetitionProblemsRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CompetitionProblemsRepository.kt new file mode 100644 index 0000000..f4d9a90 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CompetitionProblemsRepository.kt @@ -0,0 +1,10 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.codebattles.backend.entity.CompetitionsProblems + +interface CompetitionProblemsRepository : JpaRepository { + fun getAllByCompetitionId(id: Long): List + fun getFirstByCompetitionIdAndProblemId(competition_id: Long, problem_id: Long): CompetitionsProblems + fun getFirstByCompetitionIdAndId(competition_id: Long, id: Long): CompetitionsProblems +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CompetitionRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CompetitionRepository.kt new file mode 100644 index 0000000..d74c2cb --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/CompetitionRepository.kt @@ -0,0 +1,10 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.codebattles.backend.entity.Competition +import ru.codebattles.backend.entity.User + +interface CompetitionRepository : JpaRepository { + fun getByMembersContaining(user: User): List + fun existsByIdAndMembersId(id: Long, memberId: Long): Boolean +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/LeaderboardRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/LeaderboardRepository.kt new file mode 100644 index 0000000..cbd4383 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/LeaderboardRepository.kt @@ -0,0 +1,51 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import ru.codebattles.backend.entity.Competition +import ru.codebattles.backend.entity.LeaderBoardAllTasksQuery +import ru.codebattles.backend.entity.LeaderBoardScoreOrderQuery + +interface LeaderboardRepository : JpaRepository { + @Query( + """ + SELECT + a.user_id AS userId, + cp.id AS competitionProblemID, + MAX(a.score) AS maxScore + FROM public.competitions_problems AS cp + JOIN answer a ON a.competitions_problems_id = cp.id + WHERE cp.competition_id = :compId + GROUP BY a.user_id, cp.id + """, + nativeQuery = true + ) + fun getLeaderboard(@Param("compId") compId: Long): List + + @Query( + """ + SELECT + userId, + userX, + SUM(maxScore) AS score, + MAX(maxTime) AS time + FROM ( + SELECT + a.user_id AS userId, + COALESCE(u.name, 'no name') AS userX, + MAX(a.score) AS maxScore, + MAX(a.created_at) AS maxTime + FROM public.competitions_problems AS cp + JOIN answer a ON a.competitions_problems_id = cp.id + JOIN users_ u ON a.user_id = u.id + WHERE cp.competition_id = :compId + GROUP BY a.user_id, cp.id, u.name + ) AS subquery + GROUP BY userId, userX + ORDER BY score DESC, time +""", + nativeQuery = true + ) + fun getLeaderboardStats(@Param("compId") compId: Long): List +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/ProblemsRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/ProblemsRepository.kt new file mode 100644 index 0000000..c4aa61d --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/ProblemsRepository.kt @@ -0,0 +1,6 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.codebattles.backend.entity.Problem + +interface ProblemsRepository : JpaRepository \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/UserRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/UserRepository.kt new file mode 100644 index 0000000..d83acac --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/UserRepository.kt @@ -0,0 +1,13 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.codebattles.backend.entity.User + + +@Repository +interface UserRepository : JpaRepository { + fun findByMusername(username: String): User + fun findByIdIn(ids: Set): MutableSet + fun existsByMusername(username: String): Boolean +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/VariablesRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/VariablesRepository.kt new file mode 100644 index 0000000..251d55a --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/VariablesRepository.kt @@ -0,0 +1,10 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.codebattles.backend.entity.Variable + + +interface VariablesRepository : JpaRepository { + fun findByKey(key: String): Variable? + fun existsByKey(key: String): Boolean +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/AnswerService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/AnswerService.kt new file mode 100644 index 0000000..22449d6 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/AnswerService.kt @@ -0,0 +1,76 @@ +package ru.codebattles.backend.services + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Service +import ru.codebattles.backend.dto.AnswerDto +import ru.codebattles.backend.dto.mapper.AnswerMapper +import ru.codebattles.backend.entity.Answer +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.repository.AnswerRepository +import ru.codebattles.backend.repository.CheckerRepository +import ru.codebattles.backend.repository.CompetitionProblemsRepository +import ru.codebattles.backend.repository.CompetitionRepository +import ru.codebattles.backend.web.entity.SendAnswerRequest +import ru.codebattles.backend.web.entity.checker.CheckerTaskRequest +import ru.codebattles.backend.web.entity.checker.Test + +@Service +class AnswerService( + val answerRepository: AnswerRepository, + val checkerRepository: CheckerRepository, + val competitionRepository: CompetitionRepository, + val competitionProblemsRepository: CompetitionProblemsRepository, + val checkerApiService: CheckerApiService, + val answerMapper: AnswerMapper, + val objectMapper: ObjectMapper +) { + fun createAnswer(user: User, data: SendAnswerRequest) { + val checker = checkerRepository.findById(data.checker).orElseThrow() + val competitionProblem = competitionProblemsRepository.findById(data.id).orElseThrow() + val savedAnswer = answerRepository.save( + Answer( + competition = competitionProblem.competition, + checker = checker, + user = user, + code = data.src, + competitionsProblems = competitionProblem + ) + ) + + val request = CheckerTaskRequest( + source = data.src, + compiler = "python", + tests = objectMapper.readValue(competitionProblem.problem.tests, + object : TypeReference>() {} + ), + savedAnswer.id.toString() + ) + + checkerApiService.sendCheckerTask(request, checker.address) + } + + fun getAllAnswersByCompetitionsAndUser(competition: Long, user: User): List { + return answerMapper.toDtoS( + answerRepository.getAllByUserAndCompetitionId(user, competition) + ) + } + + fun getAllAnswersByCompetitionsAndUserId(competition: Long, userId: Long): List { + return answerMapper.toDtoS( + answerRepository.getAllByUserIdAndCompetitionId(userId, competition) + ) + } + + fun getLastByUserIdAndCompetitionsAnswerId(userId: Long, competitionProblemId: Long): AnswerDto { + return answerMapper.toDto( + answerRepository.getFirstByUserIdAndCompetitionsProblemsIdOrderByCreatedAtDesc(userId, competitionProblemId) + ) + } + + fun getById(id: Long): AnswerDto { + return answerMapper.toDto( + answerRepository.findById(id).orElseThrow() + ) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CheckerApiService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CheckerApiService.kt new file mode 100644 index 0000000..5519e0a --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CheckerApiService.kt @@ -0,0 +1,29 @@ +package ru.codebattles.backend.services + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate +import ru.codebattles.backend.web.entity.checker.CheckerTaskRequest + + +@Service +class CheckerApiService( + val objectMapper: ObjectMapper, +) { + + fun sendCheckerTask(payload: CheckerTaskRequest, url: String) { + val restTemplate = RestTemplate() + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + val json = objectMapper.writeValueAsString(payload) + val entity = HttpEntity(json, headers) + restTemplate.postForEntity( + url, entity, + String::class.java + ) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CheckerService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CheckerService.kt new file mode 100644 index 0000000..a70152f --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CheckerService.kt @@ -0,0 +1,40 @@ +package ru.codebattles.backend.services + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException +import ru.codebattles.backend.entity.Checker +import ru.codebattles.backend.repository.CheckerRepository +import java.io.IOException + +@Service +class CheckerService( + private val checkerRepository: CheckerRepository, + private val objectMapper: ObjectMapper, +) { + + + @Throws(IOException::class) + fun patch(id: Long, patchNode: JsonNode): Checker { + val checker: Checker = checkerRepository.findById(id).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Entity with id `$id` not found") + } + objectMapper.readerForUpdating(checker).readValue(patchNode) + return checkerRepository.save(checker) + } + + fun create(checker: Checker): Checker { + return checkerRepository.save(checker) + } + + + fun delete(id: Long): Checker? { + val checker: Checker? = checkerRepository.findById(id).orElse(null) + if (checker != null) { + checkerRepository.delete(checker) + } + return checker + } +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CompetitionService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CompetitionService.kt new file mode 100644 index 0000000..2bf97ad --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CompetitionService.kt @@ -0,0 +1,138 @@ +package ru.codebattles.backend.services + +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException +import ru.codebattles.backend.dto.CompetitionCreateDto +import ru.codebattles.backend.dto.CompetitionDto +import ru.codebattles.backend.dto.CompetitionsProblemsDto +import ru.codebattles.backend.dto.UserDto +import ru.codebattles.backend.dto.mapper.CompetitionsMapper +import ru.codebattles.backend.dto.mapper.CompetitionsProblemsMapper +import ru.codebattles.backend.dto.mapper.UserMapper +import ru.codebattles.backend.entity.Competition +import ru.codebattles.backend.entity.LeaderBoardAllTasksQuery +import ru.codebattles.backend.entity.Leaderboard +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.repository.* +import java.util.stream.Collectors + +@Service +class CompetitionService( + private val competitionRepository: CompetitionRepository, + private val userRepository: UserRepository, + private val userMapper: UserMapper, + private val competitionProblemsRepository: CompetitionProblemsRepository, + private val competitionsProblemsMapper: CompetitionsProblemsMapper, + private val competitionsMapper: CompetitionsMapper, + private val leaderboardRepository: LeaderboardRepository, + private val checkerRepository: CheckerRepository, +) { + + fun getAll(): List { + return competitionsMapper.toDtoS( + competitionRepository.findAll() + ) + } + + fun getById(id: Long): CompetitionDto { + val optionalCompetition = competitionRepository.findById(id) + if (optionalCompetition.isPresent) { + return competitionsMapper.toDto(optionalCompetition.get()) + } + + + throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + + fun getByIdNotDto(id: Long): Competition { + val optionalCompetition = competitionRepository.findById(id) + if (optionalCompetition.isPresent) { + return optionalCompetition.get() + } + + + throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + + fun getProblemsById(id: Long): List { + return competitionsProblemsMapper.toDtoS( + competitionProblemsRepository.getAllByCompetitionId(id) + ) + + } + + fun getLeaderboardById(id: Long): Leaderboard { + val leaderboard = leaderboardRepository.getLeaderboard(id) + val leaderboardScores = leaderboardRepository.getLeaderboardStats(id) + + val answersByScore: Map> = leaderboard.stream() + .collect(Collectors.groupingBy(LeaderBoardAllTasksQuery::userId)) + + return Leaderboard( + data = answersByScore, + score = leaderboardScores + ) + } + + fun getProblemById(id: Long, problemId: Long): CompetitionsProblemsDto { + return competitionsProblemsMapper.toDto( + competitionProblemsRepository.getFirstByCompetitionIdAndId(id, problemId) + ) + } + + fun patchUsers(compId: Long, usersIds: Set) { + val competition = competitionRepository.findById(compId).orElseThrow() + competition.members = userRepository.findByIdIn(usersIds) + competitionRepository.save(competition) + } + + fun patchCheckers(compId: Long, checkersIds: Set) { + val competition = competitionRepository.findById(compId).orElseThrow() + competition.checkers = checkerRepository.findByIdIn(checkersIds) + competitionRepository.save(competition) + } + + fun joinUser(compId: Long, userId: Long) { + val competition = competitionRepository.findById(compId).orElseThrow() + val user = userRepository.getById(userId) + competition.members?.add(user) + competitionRepository.save(competition) + } + + fun getUsers(compId: Long): List { + val competition = competitionRepository.findById(compId).orElseThrow() + + return userMapper.toDtoS(competition.members!!) + } + + + fun getAllByUser(user: User): List { + return competitionsMapper.toDtoS( + competitionRepository.getByMembersContaining(user) + ) + } + + fun create(competitionDto: CompetitionCreateDto, user: User): CompetitionDto { + val competition = Competition( + organizer = user, + name = competitionDto.name, + description = competitionDto.description, + ) + + competitionRepository.save( + competition + ) + + return competitionsMapper.toDto( + competition + ) + } + + fun checkAccessForCompetitionByUser(user: User, competitionId: Long): Boolean { + return ( + user.isAdmin() || + competitionRepository.existsByIdAndMembersId(competitionId, user.id!!) + ) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CompetitionsProblemsService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CompetitionsProblemsService.kt new file mode 100644 index 0000000..06248a8 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/CompetitionsProblemsService.kt @@ -0,0 +1,84 @@ +package ru.codebattles.backend.services + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException +import ru.codebattles.backend.entity.CompetitionsProblems +import ru.codebattles.backend.repository.CompetitionProblemsRepository +import ru.codebattles.backend.web.entity.CreateCompetitionProblem +import java.io.IOException +import java.util.* + +@Service +class CompetitionsProblemsService( + private val competitionProblemsRepository: CompetitionProblemsRepository, + private val objectMapper: ObjectMapper, + private val competitionService: CompetitionService, + private val problemsService: ProblemsService, +) { + + + fun getAll(pageable: Pageable): Page { + return competitionProblemsRepository.findAll(pageable) + } + + fun getOne(id: Long): CompetitionsProblems { + val competitionsProblemsOptional: Optional = competitionProblemsRepository.findById(id) + return competitionsProblemsOptional.orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Entity with id `$id` not found") + } + } + + fun getMany(ids: List): List { + return competitionProblemsRepository.findAllById(ids) + } + + fun create(data: CreateCompetitionProblem): CompetitionsProblems { + + val competitionsProblems = CompetitionsProblems( + priority = data.priority.toInt(), + slug = data.slug, + competition = competitionService.getByIdNotDto(data.competition_id), + problem = problemsService.getByIdNotDto(data.problem_id), + ) + + return competitionProblemsRepository.save(competitionsProblems) + } + + @Throws(IOException::class) + fun patch(id: Long, patchNode: JsonNode): CompetitionsProblems { + val competitionsProblems: CompetitionsProblems = competitionProblemsRepository.findById(id).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Entity with id `$id` not found") + } + objectMapper.readerForUpdating(competitionsProblems).readValue(patchNode) + return competitionProblemsRepository.save(competitionsProblems) + } + + @Throws(IOException::class) + fun patchMany(ids: List, patchNode: JsonNode): List { + val competitionsProblems: Collection = competitionProblemsRepository.findAllById(ids) + for (competitionsProblem in competitionsProblems) { + objectMapper.readerForUpdating(competitionsProblem).readValue(patchNode) + } + val resultCompetitionsProblems: List = + competitionProblemsRepository.saveAll(competitionsProblems) + return resultCompetitionsProblems.mapNotNull(CompetitionsProblems::id) + } + + fun delete(id: Long): CompetitionsProblems? { + val competitionsProblems: CompetitionsProblems? = competitionProblemsRepository.findById(id).orElse(null) + if (competitionsProblems != null) { + competitionProblemsRepository.delete(competitionsProblems) + } + return competitionsProblems + } + + fun deleteMany(ids: List) { + competitionProblemsRepository.deleteAllById(ids) + } + +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/JwtService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/JwtService.kt new file mode 100644 index 0000000..f1c49f7 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/JwtService.kt @@ -0,0 +1,58 @@ +package ru.codebattles.backend.services + + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.Keys +import org.springframework.stereotype.Service +import ru.codebattles.backend.core.properties.JwtTokenProperties +import ru.codebattles.tools.generateSecureRandomString +import java.security.Key +import java.util.* +import javax.crypto.SecretKey + +@Service +class JwtService( + private val properties: JwtTokenProperties +) { + + private val jwtSecret: Key = getSecretKey() + private val jwtExpirationMs = 3600000 // 1 hour + + + private final fun getSecretKey(): SecretKey { + var secretKey = properties.secretKey + if (secretKey == null) secretKey = generateSecureRandomString(128) + + val decodedKey = Base64.getDecoder().decode(secretKey) + return Keys.hmacShaKeyFor(decodedKey) + } + + fun generateToken(username: String): String { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(Date()) + .setExpiration(Date(System.currentTimeMillis() + jwtExpirationMs)) + .signWith(jwtSecret) + .compact() + } + + fun validateToken(token: String): Boolean { + try { + val claims = Jwts.parserBuilder() + .setSigningKey(jwtSecret) + .build() + .parseClaimsJws(token) + return !claims.body.expiration.before(Date()) + } catch (e: Exception) { + return false + } + } + + fun getUsernameFromToken(token: String): String { + val claims = Jwts.parserBuilder() + .setSigningKey(jwtSecret) + .build() + .parseClaimsJws(token) + return claims.body.subject + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/ProblemsService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/ProblemsService.kt new file mode 100644 index 0000000..99d96ce --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/ProblemsService.kt @@ -0,0 +1,76 @@ +package ru.codebattles.backend.services + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException +import ru.codebattles.backend.dto.CreateProblemDto +import ru.codebattles.backend.dto.ProblemDto +import ru.codebattles.backend.dto.mapper.CreateProblemsMapper +import ru.codebattles.backend.dto.mapper.ProblemsMapper +import ru.codebattles.backend.entity.Problem +import ru.codebattles.backend.repository.ProblemsRepository +import java.io.IOException + +@Service +class ProblemsService( + val problemsRepository: ProblemsRepository, + val problemsMapper: ProblemsMapper, + val createProblemsMapper: CreateProblemsMapper, private val objectMapper: ObjectMapper +) { + fun getById(id: Long): ProblemDto { + val optionalProblem = problemsRepository.findById(id) + if (optionalProblem.isPresent) { + return problemsMapper.toDto(optionalProblem.get()) + } + + + throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + + fun getByIdNotDto(id: Long): Problem { + val optionalProblem = problemsRepository.findById(id) + if (optionalProblem.isPresent) { + return optionalProblem.get() + } + + + throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + + + fun create(problemDto: CreateProblemDto): ProblemDto { + val problem = Problem( + name = problemDto.name, + description = problemDto.description, + inData = problemDto.inData, + outData = problemDto.outData, + tests = problemDto.tests, + examples = problemDto.examples, + ) + + println() + + val competition = problemsRepository.save(problem) + + println() + + return problemsMapper.toDto(competition) + } + + @Throws(IOException::class) + fun patch(id: Long, patchNode: JsonNode): Problem { + val problem: Problem = problemsRepository.findById(id).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Entity with id `$id` not found") + } + objectMapper.readerForUpdating(problem).readValue(patchNode) + return problemsRepository.save(problem) + } + + fun getAll(): Iterable { + return problemsMapper.toDtoS( + problemsRepository.findAll() + ) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/UserDetailService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/UserDetailService.kt new file mode 100644 index 0000000..d6d990c --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/UserDetailService.kt @@ -0,0 +1,15 @@ +package ru.codebattles.backend.services + +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.stereotype.Component +import ru.codebattles.backend.repository.UserRepository + +@Component +class UserDetailService( + private val userRepository: UserRepository, +) : UserDetailsService { + override fun loadUserByUsername(username: String?): UserDetails { + return userRepository.findByMusername(username!!) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/UserService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/UserService.kt new file mode 100644 index 0000000..c6f2aef --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/UserService.kt @@ -0,0 +1,32 @@ +package ru.codebattles.backend.services + + +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import ru.codebattles.backend.dto.CreateUserDto +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.entity.UserRole +import ru.codebattles.backend.repository.UserRepository + +@Service +class UserService( + private val userRepository: UserRepository, + private val passwordEncoder: PasswordEncoder, +) { + fun getByUsername(username: String): User { + return userRepository.findByMusername(username) + } + fun create(userDto: CreateUserDto): User { + + val user = User( + mpassword = passwordEncoder.encode(userDto.mpassword), + musername = userDto.musername, + name = userDto.name + ) + user.roles = mutableSetOf(UserRole.USER) + + userRepository.save(user) + + return user + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/AnswerController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/AnswerController.kt new file mode 100644 index 0000000..3a712d1 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/AnswerController.kt @@ -0,0 +1,57 @@ +package ru.codebattles.backend.web.controllers + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.annotation.security.RolesAllowed +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.* +import ru.codebattles.backend.dto.AnswerDto +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.entity.UserRole +import ru.codebattles.backend.services.AnswerService + +@Tag(name = "Answer", description = "Endpoints for answers") +@RestController +@RequestMapping("/api/answers") +@SecurityRequirement(name = "JWT") +class AnswerController( + val answerService: AnswerService, +) { + + @Operation( + summary = "Get answer by ID", + description = "Retrieves answer details by its ID. Requires access to the answer." + ) + @GetMapping("{id}") + fun getById(@PathVariable id: Long, @AuthenticationPrincipal user: User): AnswerDto { + val answer = answerService.getById(id) + + if (user.roles.contains(UserRole.ROLE_ADMIN) || answer.user.id == user.id) { + return answer + } + + throw AccessDeniedException("You do not have access to this answer") + } + + @Operation( + summary = "[ADMIN] Get the last answer by problem and user ID", + description = "Retrieves the most recent answer submitted by a specific user " + + "for a specific competition problem. Required admin role." + ) + @GetMapping("/last") + @RolesAllowed("ADMIN") + fun getLastSendByProblemAnswerAndUserId( + @Parameter(description = "The ID of CompetitionProblem id", required = true) + @RequestParam + compProblemId: Long, + + @Parameter(description = "The ID of the user", required = true) + @RequestParam + userId: Long, + ): AnswerDto { + return answerService.getLastByUserIdAndCompetitionsAnswerId(userId, compProblemId) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/AuthContoroller.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/AuthContoroller.kt new file mode 100644 index 0000000..b2e81e3 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/AuthContoroller.kt @@ -0,0 +1,37 @@ +package ru.codebattles.backend.web.controllers + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import ru.codebattles.backend.services.JwtService +import ru.codebattles.backend.web.entity.auth.AuthRequest +import ru.codebattles.backend.web.entity.auth.AuthResponse + +@Tag(name = "Auth", description = "Endpoints for authentication") +@RestController +@RequestMapping("/api/auth") +class AuthController( + private val authenticationManager: AuthenticationManager, + private val jwtService: JwtService +) { + + @Operation( + summary = "Login", + description = "Enter credentials to get JWT token." + ) + @PostMapping("/login") + fun login(@RequestBody authRequest: AuthRequest): AuthResponse { + val authentication = authenticationManager.authenticate( + UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password) + ) + val token = jwtService.generateToken(authentication.name) + return AuthResponse(token) + } +} + + diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CheckerController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CheckerController.kt new file mode 100644 index 0000000..c5800ed --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CheckerController.kt @@ -0,0 +1,100 @@ +package ru.codebattles.backend.web.controllers + +import com.fasterxml.jackson.databind.JsonNode +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import jakarta.annotation.security.RolesAllowed +import org.springframework.web.bind.annotation.* +import ru.codebattles.backend.dto.CheckerDto +import ru.codebattles.backend.dto.mapper.CheckerMapper +import ru.codebattles.backend.entity.Checker +import ru.codebattles.backend.repository.CheckerRepository +import ru.codebattles.backend.services.CheckerService +import ru.codebattles.backend.web.entity.CheckerCreate +import java.io.IOException + +@Tag(name = "Checkers", description = "Endpoints for managing checkers") +@RestController +@RequestMapping("/api/checkers") +@SecurityRequirement(name = "JWT") +class CheckerController( + val checkerRepository: CheckerRepository, + val checkerMapper: CheckerMapper, + private val checkerService: CheckerService, +) { + @Operation( + summary = "[ADMIN] Get all checkers", + description = "Retrieves a list of all checkers. Required admin role." + ) + @RolesAllowed("ADMIN") + @GetMapping + fun getAll(): List { + return checkerMapper.toDtoS( + checkerRepository.findAll() + ) + } + + @Operation( + summary = "[ADMIN] Get checker by ID", + description = "Retrieves a checker by its ID. Required admin role." + ) + @RolesAllowed("ADMIN") + @GetMapping("/{id}") + fun getById(@PathVariable id: Long): CheckerDto { + return checkerMapper.toDto( + checkerRepository.getById(id) + ) + } + + @Operation( + summary = "[ADMIN] Get checker by ID (extra fields)", + description = "Retrieves a checker by its ID with admin-level access. Required admin role." + ) + @RolesAllowed("ADMIN") + @GetMapping("/{id}/admin") + fun getByIdADMIN(@PathVariable id: Long): Checker { + val checkerOptional = checkerRepository.findById(id) + return checkerOptional.get() + } + + @Operation( + summary = "[ADMIN] Update a checker", + description = "Applies partial updates to a checker by its ID. Required admin role." + ) + @RolesAllowed("ADMIN") + @PatchMapping("/{id}") + @Throws(IOException::class) + fun patch(@PathVariable id: Long, @RequestBody patchNode: JsonNode): Checker { + return checkerService.patch(id, patchNode) + } + + @Operation( + summary = "[ADMIN] Create a checker", + description = "Creates a new checker. Required admin role." + ) + @RolesAllowed("ADMIN") + @PostMapping + fun create(@RequestBody checkerCreateDto: CheckerCreate): Checker { + + val checker = Checker( + checkerCreateDto.displayName, + checkerCreateDto.languageHighlightName, + checkerCreateDto.address, + ) + + return checkerService.create(checker) + } + + @Operation( + summary = "[ADMIN] Delete a checker", + description = "Deletes a specific checker by its ID. Required admin role." + ) + @RolesAllowed("ADMIN") + @DeleteMapping("/{id}") + fun delete(@PathVariable id: Long): Checker? { + return checkerService.delete(id) + } + + +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CheckerSystemEndpointController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CheckerSystemEndpointController.kt new file mode 100644 index 0000000..6f0fc63 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CheckerSystemEndpointController.kt @@ -0,0 +1,46 @@ +package ru.codebattles.backend.web.controllers + +import com.fasterxml.jackson.databind.ObjectMapper +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController +import ru.codebattles.backend.entity.AnswerStatus +import ru.codebattles.backend.repository.AnswerRepository +import ru.codebattles.backend.web.entity.checker.CheckerCallback + +@Tag(name = "Checker system API", description = "Endpoints for checker system") +@RestController +class CheckerSystemEndpointController( + val answerRepository: AnswerRepository, + val objectMapper: ObjectMapper +) { + @Operation( + summary = "(Internal method) Handle checker system callback", + description = "Processes the callback from the checker system, updates the answer status, and calculates the score. " + + "Used only for checkers. Access disabled if used via gateway" + ) + @PostMapping("/api/check_system_callback") + fun checkerCallBack(@RequestBody data: CheckerCallback) { + println() + + val answer = answerRepository.getById(data.meta) + + val countOfTests = data.results.size + val countOfSuccessTests = data.results.count { it.success } + + var score = 0 + if (countOfTests > 0) { + score = countOfSuccessTests / countOfTests * 100 + } + + answer.result = objectMapper.writeValueAsString(data) + answer.status = AnswerStatus.COMPLETED + answer.score = score + + + + answerRepository.save(answer) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CompetitionsController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CompetitionsController.kt new file mode 100644 index 0000000..b53bdc1 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CompetitionsController.kt @@ -0,0 +1,180 @@ +package ru.codebattles.backend.web.controllers + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.annotation.security.RolesAllowed +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.* +import ru.codebattles.backend.annotations.CompetitionAccessRequired +import ru.codebattles.backend.annotations.CompetitionId +import ru.codebattles.backend.dto.* +import ru.codebattles.backend.dto.mapper.CompetitionsMapper +import ru.codebattles.backend.entity.Leaderboard +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.repository.CompetitionRepository +import ru.codebattles.backend.services.AnswerService +import ru.codebattles.backend.services.CompetitionService +import ru.codebattles.backend.web.entity.CompetitionEditDto +import ru.codebattles.backend.web.entity.EditUsersRequest +import ru.codebattles.backend.web.entity.SendAnswerRequest + +@Tag(name = "Competitions", description = "Endpoints for managing competitions") +@RestController +@RequestMapping("/api/competitions") +@SecurityRequirement(name = "JWT") +class CompetitionsController ( + private val competitionMapper: CompetitionsMapper +) { + @Autowired + private lateinit var competitionRepository: CompetitionRepository + + @Autowired + private lateinit var competitionService: CompetitionService + + @Autowired + private lateinit var answerService: AnswerService + + @Operation( + summary = "[ADMIN] Create a new competition", + description = "Creates a new competition object. Required admin role." + ) + @RolesAllowed("ADMIN") + @PostMapping + fun create(@RequestBody instance: CompetitionCreateDto, @AuthenticationPrincipal user: User): CompetitionDto { + return competitionService.create(instance, user) + } + + @Operation( + summary = "Get competition by ID", + description = "Retrieves competition details by its ID. Requires access to the competition." + ) + @CompetitionAccessRequired + @GetMapping("{compId}") + fun getById(@CompetitionId @PathVariable compId: Long): CompetitionDto { + return competitionService.getById(compId) + } + + @Operation( + summary = "Submit an answer", + description = "Allows a user to submit an answer for a specific competition problem." + ) + @CompetitionAccessRequired + @PostMapping("{compId}/send") + fun send( + @CompetitionId @PathVariable compId: Long, + @AuthenticationPrincipal user: User, + @RequestBody data: SendAnswerRequest + ): String { + answerService.createAnswer(user, data) + return "aboba" + } + + @Operation( + summary = "Get all answers", + description = "Retrieves all answers submitted by the authenticated user for a specific competition." + ) + @CompetitionAccessRequired + @GetMapping("{compId}/sends") + fun getAnswers( + @CompetitionId @PathVariable compId: Long, + @AuthenticationPrincipal user: User, + ): List { + return answerService.getAllAnswersByCompetitionsAndUserId(compId, user.id!!) + } + + @Operation( + summary = "Get competition problems", + description = "Retrieves all problems associated with a specific competition." + ) + @CompetitionAccessRequired + @GetMapping("{compId}/problems") + fun getProblemsByCompetition(@CompetitionId @PathVariable compId: Long): List { + return competitionService.getProblemsById(compId) + } + + @Operation( + summary = "Get competition leaderboard", + description = "Retrieves the leaderboard for a specific competition." + ) + @CompetitionAccessRequired + @GetMapping("{compId}/leaderboard") + fun leaderboard(@CompetitionId @PathVariable compId: Long): Leaderboard { + return competitionService.getLeaderboardById(compId) + } + + @Operation( + summary = "[ADMIN] Edit competition users", + description = "Updates the list of users participating in a specific competition. Required admin role." + ) + @PutMapping("{compId}/users") + @RolesAllowed("ADMIN") + @ResponseStatus(HttpStatus.ACCEPTED) + fun editUsers(@PathVariable compId: Long, @RequestBody data: EditUsersRequest) { + competitionService.patchUsers(compId, data.usersIds) + } + + @RolesAllowed("ADMIN") + @PutMapping("{compId}") + fun update(@PathVariable compId: Long, @RequestBody profileData: CompetitionEditDto): CompetitionDto { + val competition = competitionRepository.getById(compId) + + competitionMapper.update(profileData, competition) + competitionRepository.save(competition) + + return competitionMapper.toDto(competition) + } + + + @Operation( + summary = "[ADMIN] Edit competition checkers", + description = "Updates the list of checkers for a specific competition. Required admin role." + ) + @RolesAllowed("ADMIN") + @PutMapping("{compId}/checkers") + @ResponseStatus(HttpStatus.ACCEPTED) + fun editCheckers(@PathVariable compId: Long, @RequestBody data: EditUsersRequest) { + competitionService.patchCheckers(compId, data.usersIds) + } + + @Operation( + summary = "[ADMIN] Get competition users", + description = "Retrieves the list of users participating in a specific competition. Required admin role." + ) + @RolesAllowed("ADMIN") + @GetMapping("{compId}/users") + fun getUsers(@PathVariable compId: Long): List { + return competitionService.getUsers(compId) + } + + @Operation( + summary = "Get competition problem by ID", + description = "Retrieves a specific problem by its ID within a competition." + ) + @CompetitionAccessRequired + @GetMapping("{compId}/problems/{id}") + fun getProblemsByIdByCompetition(@CompetitionId @PathVariable compId: Long, @PathVariable id: Long): CompetitionsProblemsDto { + return competitionService.getProblemById(compId, id) + } + + @Operation( + summary = "Get competitions available for user", + description = "Retrieves all competitions accessible to the authenticated user." + ) + @GetMapping("/me") + fun getAllAvaliableForUser(@AuthenticationPrincipal user: User): List { + return competitionService.getAllByUser(user) + } + + @Operation( + summary = "[ADMIN] Get all competitions", + description = "Retrieves a list of all competitions. Required admin role." + ) + @RolesAllowed("ADMIN") + @GetMapping + fun getAll(): List { + return competitionService.getAll() + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CompetitionsProblemsController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CompetitionsProblemsController.kt new file mode 100644 index 0000000..3211a35 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/CompetitionsProblemsController.kt @@ -0,0 +1,123 @@ +package ru.codebattles.backend.web.controllers + +import com.fasterxml.jackson.databind.JsonNode +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.annotation.security.RolesAllowed +import org.springdoc.core.annotations.ParameterObject +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.web.PagedModel +import org.springframework.web.bind.annotation.* +import ru.codebattles.backend.dto.CompetitionsProblemsDto +import ru.codebattles.backend.dto.mapper.CompetitionsProblemsMapper +import ru.codebattles.backend.entity.CompetitionsProblems +import ru.codebattles.backend.services.CompetitionsProblemsService +import ru.codebattles.backend.web.entity.CreateCompetitionProblem +import java.io.IOException + +@Tag(name = "Competition Problems", description = "Endpoints for managing competition problems") +@RestController +@RequestMapping("/api/competitionsProblems") +@SecurityRequirement(name = "JWT") +class CompetitionsProblemsController( + private val competitionsProblemsService: CompetitionsProblemsService, + private val competitionsProblemsMapper: CompetitionsProblemsMapper, +) { + @Operation( + summary = "[ADMIN] Get all competition problems", + description = "Retrieves a paginated list of all competition problems. Required admin role." + ) + @Deprecated("Dont use global getter") + @RolesAllowed("ADMIN") + @GetMapping + fun getAll(@ParameterObject pageable: Pageable): PagedModel { + val competitionsProblems: Page = competitionsProblemsService.getAll(pageable) + return PagedModel(competitionsProblems) + } + + @Operation( + summary = "[ADMIN] Get competition problem by ID", + description = "Retrieves details of a specific competition problem by its ID. Required admin role." + ) + @RolesAllowed("ADMIN") + @GetMapping("/{id}") + fun getOne(@PathVariable id: Long): CompetitionsProblemsDto { + + return competitionsProblemsMapper.toDto( + competitionsProblemsService.getOne(id) + ) + } + + @Operation( + summary = "[ADMIN] Get multiple competition problems", + description = "Retrieves details of multiple competition problems by their IDs. Required admin role." + ) + @RolesAllowed("ADMIN") + @GetMapping("/by-ids") + fun getMany(@RequestParam ids: List): List { + return competitionsProblemsMapper.toDtoS( + competitionsProblemsService.getMany(ids) + ) + } + + @Operation( + summary = "[ADMIN] Create a competition problem", + description = "Creates a new competition problem." + ) + @RolesAllowed("ADMIN") + @PostMapping + fun create(@RequestBody data: CreateCompetitionProblem): CompetitionsProblemsDto { + return competitionsProblemsMapper.toDto( + competitionsProblemsService.create(data) + ) + } + + @Operation( + summary = "[ADMIN] Update a competition problem", + description = "Applies partial updates to a specific competition problem. Required admin role." + ) + @RolesAllowed("ADMIN") + @PatchMapping("/{id}") + @Throws(IOException::class) + fun patch(@PathVariable id: Long, @RequestBody patchNode: JsonNode): CompetitionsProblemsDto { + return competitionsProblemsMapper.toDto( + competitionsProblemsService.patch(id, patchNode) + ) + } + + @Operation( + summary = "[ADMIN] Update multiple competition problems", + description = "Applies partial updates to multiple competition problems. Required admin role." + ) + @RolesAllowed("ADMIN") + @PatchMapping + @Throws(IOException::class) + fun patchMany(@RequestParam ids: List, @RequestBody patchNode: JsonNode): List { + return competitionsProblemsService.patchMany(ids, patchNode) + } + + + @Operation( + summary = "[ADMIN] Delete a competition problem", + description = "Deletes a specific competition problem by its ID. Required admin role." + ) + @RolesAllowed("ADMIN") + @DeleteMapping("/{id}") + fun delete(@PathVariable id: Long): CompetitionsProblemsDto { + return competitionsProblemsMapper.toDto( + competitionsProblemsService.delete(id)!! + ) + } + + @Operation( + summary = "[ADMIN] Delete multiple competition problems", + description = "Deletes multiple competition problems by their IDs. Required admin role." + ) + @RolesAllowed("ADMIN") + @DeleteMapping + fun deleteMany(@RequestParam ids: List) { + competitionsProblemsService.deleteMany(ids) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/GlobalExceptionHandler.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/GlobalExceptionHandler.kt new file mode 100644 index 0000000..db317f7 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/GlobalExceptionHandler.kt @@ -0,0 +1,33 @@ +package ru.codebattles.backend.web.controllers + +import org.springframework.http.HttpStatus +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.core.AuthenticationException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestControllerAdvice +import ru.codebattles.backend.web.entity.errors.AccessDeniedResponse +import ru.codebattles.backend.web.entity.errors.InternalServerErrorResponse +import ru.codebattles.backend.web.entity.errors.UnauthorizedResponse + + +@RestControllerAdvice +class GlobalExceptionHandler { + @ExceptionHandler(Throwable::class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + fun handleThrowable(e: Throwable): InternalServerErrorResponse { + return InternalServerErrorResponse(message = e.message) + } + + @ExceptionHandler(AccessDeniedException::class) + @ResponseStatus(HttpStatus.FORBIDDEN) + fun handleAccessDeniedException(e: AccessDeniedException): AccessDeniedResponse { + return AccessDeniedResponse(message = e.message) + } + + @ExceptionHandler(AuthenticationException::class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + fun handleAuthenticationException(e: AuthenticationException): UnauthorizedResponse { + return UnauthorizedResponse(message = e.message) + } +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/PingPongController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/PingPongController.kt new file mode 100644 index 0000000..2653df2 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/PingPongController.kt @@ -0,0 +1,22 @@ +package ru.codebattles.backend.web.controllers + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import ru.codebattles.backend.web.entity.RenderedError + +@Tag(name = "Ping Pong", description = "Endpoints for ping-pong testing") +@RestController +@RequestMapping("/api/ping") +class PingPongController { + @Operation( + summary = "Ping endpoint", + description = "Returns a 'pong' response to test the API availability." + ) + @GetMapping + fun ping(): RenderedError { + return RenderedError(detail = "pong") + } +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/ProblemsController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/ProblemsController.kt new file mode 100644 index 0000000..02beaf8 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/ProblemsController.kt @@ -0,0 +1,82 @@ +package ru.codebattles.backend.web.controllers + +import com.fasterxml.jackson.databind.JsonNode +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import jakarta.annotation.security.RolesAllowed +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.* +import ru.codebattles.backend.dto.CreateProblemDto +import ru.codebattles.backend.dto.ProblemDto +import ru.codebattles.backend.entity.Problem +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.repository.ProblemsRepository +import ru.codebattles.backend.services.ProblemsService +import java.io.IOException + +@Tag(name = "Problems", description = "Endpoints for managing problems") +@RestController +@RequestMapping("/api/problems") +@SecurityRequirement(name = "JWT") +class ProblemsController { + + @Autowired + private lateinit var problemsRepository: ProblemsRepository + + @Autowired + private lateinit var problemsService: ProblemsService + + @Operation( + summary = "[ADMIN] Create a new problem", + description = "Creates a new problem using the provided data." + ) + @RolesAllowed("ADMIN") + @PostMapping + fun create(@RequestBody instance: CreateProblemDto, @AuthenticationPrincipal user: User): ProblemDto { + return problemsService.create(instance) + } + + @Operation( + summary = "[ADMIN] Get problem by ID", + description = "Retrieves a problem by its ID. Required admin role" + ) + @RolesAllowed("ADMIN") + @GetMapping("{id}") + fun getById(@PathVariable id: Long): ProblemDto { + return problemsService.getById(id) + } + + @Operation( + summary = "[ADMIN] Get problem by ID (extra fields)", + description = "Retrieves a problem by its ID with admin-level access. Required admin role" + ) + @RolesAllowed("ADMIN") + @GetMapping("{id}/admin") + fun getByIdAdmin(@PathVariable id: Long): Problem { + val problemOptional = problemsRepository.findById(id) + return problemOptional.get() + } + + @Operation( + summary = "[ADMIN] Get all problems", + description = "Retrieves a list of all problems." + ) + @RolesAllowed("ADMIN") + @GetMapping + fun getAll(): Iterable { + return problemsService.getAll() + } + + @Operation( + summary = "[ADMIN] Update a problem", + description = "Applies partial updates to a problem by its ID." + ) + @RolesAllowed("ADMIN") + @PatchMapping("/{id}") + @Throws(IOException::class) + fun patch(@PathVariable id: Long, @RequestBody patchNode: JsonNode): Problem { + return problemsService.patch(id, patchNode) + } +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/ProfileController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/ProfileController.kt new file mode 100644 index 0000000..168296e --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/ProfileController.kt @@ -0,0 +1,37 @@ +package ru.codebattles.backend.web.controllers + +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.* +import ru.codebattles.backend.dto.UserProfileDto +import ru.codebattles.backend.dto.UserProfileEditDto +import ru.codebattles.backend.dto.mapper.UserMapper +import ru.codebattles.backend.dto.mapper.UserProfileMapper +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.repository.UserRepository + +@Tag(name = "Profile", description = "Endpoints for current profile") +@RestController +@RequestMapping("/api/profile") +@SecurityRequirement(name = "JWT") +class ProfileController( + private val userRepository: UserRepository, + private val userMapper: UserMapper, + private val userProfileMapper: UserProfileMapper, +) { + + @PutMapping + fun updateProfile(@RequestBody profileData: UserProfileEditDto, @AuthenticationPrincipal user: User): UserProfileDto { + userMapper.updateUserProfile(profileData, user) + userRepository.save(user) + + return userProfileMapper.toDto(user) + } + + @GetMapping + fun getMe(@AuthenticationPrincipal user: User): UserProfileDto { + return userProfileMapper.toDto(user) + } + +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/UsersController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/UsersController.kt new file mode 100644 index 0000000..8090dce --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/UsersController.kt @@ -0,0 +1,81 @@ +package ru.codebattles.backend.web.controllers + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.annotation.security.RolesAllowed +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.* +import ru.codebattles.backend.dto.CreateUserDto +import ru.codebattles.backend.dto.UserDto +import ru.codebattles.backend.dto.mapper.UserMapper +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.repository.UserRepository +import ru.codebattles.backend.services.CompetitionService +import ru.codebattles.backend.services.UserService +import ru.codebattles.backend.web.entity.LinkUserRequest +import ru.codebattles.backend.web.entity.OkResponse +import java.util.* + +@Tag(name = "Users", description = "Endpoints for managing users") +@RestController +@RequestMapping("/api/users") +@SecurityRequirement(name = "JWT") +class UsersController( + val userRepository: UserRepository, + val userMapper: UserMapper, + private val userService: UserService, + private val competitionService: CompetitionService, +) { + @Operation( + summary = "Get current user", + description = "Retrieves current user." + ) + @GetMapping("me") + fun getProfile(@AuthenticationPrincipal user: User): Optional { + return userRepository.findById(user.id!!) + } + + + @Operation( + summary = "[ADMIN] Get all users", + description = "Retrieves a list of all users. Required admin role" + ) + @RolesAllowed("ADMIN") + @GetMapping + fun getAll(@AuthenticationPrincipal user: User): List { + return userMapper.toDtoS( + userRepository.findAll() + ) + } + + @Operation( + summary = "[ADMIN] Create user", + description = "Create user with provided data. Required admin role." + ) + @RolesAllowed("ADMIN") + @PostMapping + fun create(@RequestBody(required = true) userDto: CreateUserDto): UserDto { + + return userMapper.toDto( + userService.create(userDto) + ) + } + + @Operation( + summary = "[ADMIN] Link user to competition", + description = "Links a user to a competition. Required admin role." + ) + @RolesAllowed("ADMIN") + @PostMapping("link") + fun linkUser(@RequestBody(required = true) linkReq: LinkUserRequest): OkResponse { + + competitionService.joinUser( + linkReq.competitionId, + linkReq.userId, + ) + + return OkResponse() + } + +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/libs/SwaggerRedirectController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/libs/SwaggerRedirectController.kt new file mode 100644 index 0000000..86b7479 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/libs/SwaggerRedirectController.kt @@ -0,0 +1,10 @@ +package ru.codebattles.backend.web.controllers.libs + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +@Controller +class SwaggerRedirectController { + @GetMapping("/swagger-ui") + fun redirectToNewUrl() = "redirect:/swagger-ui/index.html" +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CheckerCreate.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CheckerCreate.kt new file mode 100644 index 0000000..0eba3f9 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CheckerCreate.kt @@ -0,0 +1,15 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Details for creating a new checker") +data class CheckerCreate( + @Schema(description = "Display name of the checker", example = "Python Checker") + val displayName: String, + + @Schema(description = "Language highlight name for the checker", example = "python") + val languageHighlightName: String, + + @Schema(description = "Address of the checker service", example = "http://localhost:8080") + val address: String, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CompetitionEditDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CompetitionEditDto.kt new file mode 100644 index 0000000..f9ee869 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CompetitionEditDto.kt @@ -0,0 +1,19 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class CompetitionEditDto( + + @Schema(description = "Name of the competition", example = "Code Battles 2023") + val name: String, + + @Schema(description = "Description of the competition", example = "A competitive coding event") + val description: String, + + @Schema(description = "Start time of the competition", example = "2023-01-01T10:00:00") + val startedAt: LocalDateTime? = null, + + @Schema(description = "End time of the competition", example = "2023-01-01T18:00:00") + val endedAt: LocalDateTime? = null +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CreateCompetitionProblem.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CreateCompetitionProblem.kt new file mode 100644 index 0000000..d3aa1d4 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/CreateCompetitionProblem.kt @@ -0,0 +1,18 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Details for creating a competition problem") +class CreateCompetitionProblem( + @Schema(description = "Priority of the problem in the competition", example = "1") + val priority: Long, + + @Schema(description = "Slug identifier for the problem", example = "problem-slug") + val slug: String, + + @Schema(description = "ID of the competition", example = "1001") + val competition_id: Long, + + @Schema(description = "ID of the problem", example = "2002") + val problem_id: Long, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/EditUsersRequest.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/EditUsersRequest.kt new file mode 100644 index 0000000..47dbafc --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/EditUsersRequest.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Request to edit users") +data class EditUsersRequest( + @Schema(description = "Set of user IDs to be edited", example = "1,2,3") + val usersIds: Set +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/LinkUserRequest.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/LinkUserRequest.kt new file mode 100644 index 0000000..fe69c94 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/LinkUserRequest.kt @@ -0,0 +1,12 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Request to link a user to a competition") +data class LinkUserRequest( + @Schema(description = "ID of the user", example = "1") + val userId: Long, + + @Schema(description = "ID of the competition", example = "1001") + val competitionId: Long, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/OkResponse.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/OkResponse.kt new file mode 100644 index 0000000..881925c --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/OkResponse.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Response indicating success") +data class OkResponse( + @Schema(description = "Status of the response", example = "OK") + val status: String = "OK", +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/ProblemCreateRequest.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/ProblemCreateRequest.kt new file mode 100644 index 0000000..0af68c1 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/ProblemCreateRequest.kt @@ -0,0 +1,27 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Request to create a problem") +data class ProblemCreateRequest( + @Schema(description = "Name of the problem", example = "Sum of Two Numbers") + val name: String, + + @Schema(description = "Description of the problem", example = "Calculate the sum of two integers.") + val description: String, + + @Schema(description = "Input data for the problem", example = "2 3") + val inData: String, + + @Schema(description = "Expected output data for the problem", example = "5") + val outData: String, + + @Schema(description = "Test cases for the problem", example = "[{\"in\": \"2 3\", \"out\": \"5\"}]") + val tests: String, + + @Schema(description = "Example cases for the problem", example = "[{\"in\": \"1 1\", \"out\": \"2\"}]") + val examples: String, + + @Schema(description = "Whether the problem is public", example = "true") + val public: Boolean? = false +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/RenderedError.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/RenderedError.kt new file mode 100644 index 0000000..b5d42bf --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/RenderedError.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Details of an error response") +data class RenderedError( + @Schema(description = "Detailed error message", example = "Invalid input data") + val detail: String +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/SendAnswerRequest.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/SendAnswerRequest.kt new file mode 100644 index 0000000..9444f7e --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/SendAnswerRequest.kt @@ -0,0 +1,15 @@ +package ru.codebattles.backend.web.entity + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Request to send an answer") +data class SendAnswerRequest( + @Schema(description = "ID of the checker", example = "1") + val checker: Long, + + @Schema(description = "Source code submitted as the answer", example = "print(input())") + val src: String, + + @Schema(description = "ID of the answer", example = "1001") + val id: Long, +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/auth/AuthRequest.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/auth/AuthRequest.kt new file mode 100644 index 0000000..1802e37 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/auth/AuthRequest.kt @@ -0,0 +1,11 @@ +package ru.codebattles.backend.web.entity.auth +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Request for user authentication") +data class AuthRequest( + @Schema(description = "Username of the user", example = "john_doe") + val username: String, + + @Schema(description = "Password of the user", example = "password123") + val password: String +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/auth/AuthResponse.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/auth/AuthResponse.kt new file mode 100644 index 0000000..a227331 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/auth/AuthResponse.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.web.entity.auth + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Response containing authentication token") +data class AuthResponse( + @Schema(description = "JWT token for authentication", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + val token: String +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/CheckerCallback.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/CheckerCallback.kt new file mode 100644 index 0000000..e0d2e45 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/CheckerCallback.kt @@ -0,0 +1,13 @@ +package ru.codebattles.backend.web.entity.checker + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Callback response from the checker") +@JvmRecord +data class CheckerCallback( + @Schema(description = "List of program results") + val results: List, + + @Schema(description = "Metadata associated with the callback", example = "12345") + val meta: Long +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/CheckerTaskRequest.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/CheckerTaskRequest.kt new file mode 100644 index 0000000..76c1c3d --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/CheckerTaskRequest.kt @@ -0,0 +1,27 @@ +package ru.codebattles.backend.web.entity.checker + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Test case for the checker") +data class Test( + @Schema(description = "Input for the test case", example = "2 3") + val `in`: String, + + @Schema(description = "Expected output for the test case", example = "5") + val out: String, +) + +@Schema(description = "Request to create a checker task") +data class CheckerTaskRequest( + @Schema(description = "Source code to be checked", example = "print(input())") + val source: String, + + @Schema(description = "Compiler to be used", example = "python3") + val compiler: String, + + @Schema(description = "List of test cases") + val tests: List, + + @Schema(description = "Metadata for the task", example = "task-123") + val meta: String +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/ProcessEndStatus.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/ProcessEndStatus.kt new file mode 100644 index 0000000..555f1cd --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/ProcessEndStatus.kt @@ -0,0 +1,18 @@ +package ru.codebattles.backend.web.entity.checker + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Status of the process execution") +enum class ProcessEndStatus(private val msg: String) { + SUCCESS("OK"), + RUNTIME_ERROR("RE"), + COMPILE_ERROR("CE"), + TIME_LIMIT("TL"), + WRONG_ANSWER("WA"), + NOT_EXECUTED("NE"), + ; + + override fun toString(): String { + return msg + } +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/ProgramResult.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/ProgramResult.kt new file mode 100644 index 0000000..45327ff --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/checker/ProgramResult.kt @@ -0,0 +1,19 @@ +package ru.codebattles.backend.web.entity.checker + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Result of a program execution") +@JvmRecord +data class ProgramResult( + @Schema(description = "Indicates if the execution was successful", example = "true") + val success: Boolean, + + @Schema(description = "Output of the program", example = "Hello, World!") + val out: String, + + @Schema(description = "Status message of the execution", example = "OK") + val msg: ProcessEndStatus, + + @Schema(description = "Execution time in milliseconds", example = "150") + val time: Int +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/AccessDeniedResponse.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/AccessDeniedResponse.kt new file mode 100644 index 0000000..c4c35fe --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/AccessDeniedResponse.kt @@ -0,0 +1,20 @@ +package ru.codebattles.backend.web.entity.errors + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema( + description = "Response returned when the user does not have permission to access the requested resource." +) +class AccessDeniedResponse( + @Schema( + description = "Detailed message about the error.", + example = "You do not have permission to access this resource." + ) + val message: String? = "Forbidden", + + @Schema( + description = "HTTP status code representing the error.", + example = "403" + ) + val code: Int? = 403 +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/InternalServerErrorResponse.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/InternalServerErrorResponse.kt new file mode 100644 index 0000000..e91e444 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/InternalServerErrorResponse.kt @@ -0,0 +1,21 @@ +package ru.codebattles.backend.web.entity.errors + +import io.swagger.v3.oas.annotations.media.Schema + + +@Schema( + description = "Response returned when an unexpected error occurs on the server." +) +class InternalServerErrorResponse( + @Schema( + description = "Detailed message about the error.", + example = "An unexpected error occurred. Please try again later." + ) + val message: String? = "Internal Server Error", + + @Schema( + description = "HTTP status code representing the error.", + example = "500" + ) + val code: Int? = 500 +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/UnauthorizedResponse.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/UnauthorizedResponse.kt new file mode 100644 index 0000000..46a94c3 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/entity/errors/UnauthorizedResponse.kt @@ -0,0 +1,20 @@ +package ru.codebattles.backend.web.entity.errors + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema( + description = "Response returned when the user is not authenticated." +) +class UnauthorizedResponse( + @Schema( + description = "Detailed message about the error.", + example = "Authentication is required to access this resource." + ) + val message: String? = "Unauthorized", + + @Schema( + description = "HTTP status code representing the error.", + example = "401" + ) + val code: Int? = 401 +) \ No newline at end of file diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/tools/RandomStringGenerator.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/tools/RandomStringGenerator.kt new file mode 100644 index 0000000..407134b --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/tools/RandomStringGenerator.kt @@ -0,0 +1,11 @@ +package ru.codebattles.tools + +import java.security.SecureRandom + +fun generateSecureRandomString(length: Int): String { + val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + val secureRandom = SecureRandom() + return (1..length) + .map { chars[secureRandom.nextInt(chars.length)] } + .joinToString("") +} \ No newline at end of file diff --git a/BACKEND_V2/src/main/resources/application.properties b/BACKEND_V2/src/main/resources/application.properties new file mode 100644 index 0000000..8ccc9ad --- /dev/null +++ b/BACKEND_V2/src/main/resources/application.properties @@ -0,0 +1,11 @@ +spring.application.name=BACKEND_V2 +spring.datasource.url=jdbc:postgresql://localhost:5432/codebattles +spring.datasource.username=postgres +spring.datasource.password=admin +spring.jpa.hibernate.ddl-auto=none +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +#codebattles.jwt.secret-key=alexalexalexalexalexalexalexalexalexalexalexalexalexalexalexalexalexalexalexalex + +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migrations \ No newline at end of file diff --git a/BACKEND_V2/src/main/resources/db/migrations/V1__init.sql b/BACKEND_V2/src/main/resources/db/migrations/V1__init.sql new file mode 100644 index 0000000..513a2f4 --- /dev/null +++ b/BACKEND_V2/src/main/resources/db/migrations/V1__init.sql @@ -0,0 +1,134 @@ +CREATE TABLE answer +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + competition_id BIGINT, + user_id BIGINT, + status VARCHAR(255), + score INTEGER, + code VARCHAR(255), + checker_id BIGINT, + result VARCHAR(255), + competitions_problems_id BIGINT, + CONSTRAINT pk_answer PRIMARY KEY (id) +); + +CREATE TABLE checker +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + display_name VARCHAR(255), + language_highlight_name VARCHAR(255), + address VARCHAR(255), + CONSTRAINT pk_checker PRIMARY KEY (id) +); + +CREATE TABLE competitions +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + organizer_id BIGINT, + name VARCHAR(255) NOT NULL, + description VARCHAR(1000) NOT NULL, + started_at TIMESTAMP WITHOUT TIME ZONE, + ended_at TIMESTAMP WITHOUT TIME ZONE, + show_rating BOOLEAN NOT NULL, + CONSTRAINT pk_competitions PRIMARY KEY (id) +); + +CREATE TABLE competitions_checkers +( + competition_id BIGINT NOT NULL, + checkers_id BIGINT NOT NULL, + CONSTRAINT pk_competitions_checkers PRIMARY KEY (competition_id, checkers_id) +); + +CREATE TABLE competitions_members +( + competition_id BIGINT NOT NULL, + members_id BIGINT NOT NULL, + CONSTRAINT pk_competitions_members PRIMARY KEY (competition_id, members_id) +); + +CREATE TABLE competitions_problems +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + priority INTEGER NOT NULL, + slug VARCHAR(255), + competition_id BIGINT, + problem_id BIGINT, + CONSTRAINT pk_competitionsproblems PRIMARY KEY (id) +); + +CREATE TABLE problem +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255), + description TEXT, + in_data TEXT, + out_data TEXT, + tests TEXT, + examples TEXT, + CONSTRAINT pk_problem PRIMARY KEY (id) +); + +CREATE TABLE users_ +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + username VARCHAR(255), + password VARCHAR(255), + name VARCHAR(255), + roles VARCHAR(255), + CONSTRAINT pk_users_ PRIMARY KEY (id) +); + +CREATE TABLE variables +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + value VARCHAR(255), + CONSTRAINT pk_variables PRIMARY KEY (id) +); + +ALTER TABLE variables + ADD CONSTRAINT uc_variables_name UNIQUE (name); + +ALTER TABLE answer + ADD CONSTRAINT FK_ANSWER_ON_CHECKER FOREIGN KEY (checker_id) REFERENCES checker (id); + +ALTER TABLE answer + ADD CONSTRAINT FK_ANSWER_ON_COMPETITION FOREIGN KEY (competition_id) REFERENCES competitions (id); + +ALTER TABLE answer + ADD CONSTRAINT FK_ANSWER_ON_COMPETITIONSPROBLEMS FOREIGN KEY (competitions_problems_id) REFERENCES competitions_problems (id); + +ALTER TABLE answer + ADD CONSTRAINT FK_ANSWER_ON_USER FOREIGN KEY (user_id) REFERENCES users_ (id); + +ALTER TABLE competitions_problems + ADD CONSTRAINT FK_COMPETITIONSPROBLEMS_ON_COMPETITION FOREIGN KEY (competition_id) REFERENCES competitions (id); + +ALTER TABLE competitions_problems + ADD CONSTRAINT FK_COMPETITIONSPROBLEMS_ON_PROBLEM FOREIGN KEY (problem_id) REFERENCES problem (id); + +ALTER TABLE competitions + ADD CONSTRAINT FK_COMPETITIONS_ON_ORGANIZER FOREIGN KEY (organizer_id) REFERENCES users_ (id); + +ALTER TABLE competitions_checkers + ADD CONSTRAINT fk_comche_on_checker FOREIGN KEY (checkers_id) REFERENCES checker (id); + +ALTER TABLE competitions_checkers + ADD CONSTRAINT fk_comche_on_competition FOREIGN KEY (competition_id) REFERENCES competitions (id); + +ALTER TABLE competitions_members + ADD CONSTRAINT fk_commem_on_competition FOREIGN KEY (competition_id) REFERENCES competitions (id); + +ALTER TABLE competitions_members + ADD CONSTRAINT fk_commem_on_user FOREIGN KEY (members_id) REFERENCES users_ (id); \ No newline at end of file diff --git a/BACKEND_V2/src/main/resources/db/migrations/V2__add_competitions_flags.sql b/BACKEND_V2/src/main/resources/db/migrations/V2__add_competitions_flags.sql new file mode 100644 index 0000000..45c46f3 --- /dev/null +++ b/BACKEND_V2/src/main/resources/db/migrations/V2__add_competitions_flags.sql @@ -0,0 +1,11 @@ +ALTER TABLE competitions + ADD show_input BOOLEAN; + +ALTER TABLE competitions + ADD show_output BOOLEAN; + +ALTER TABLE competitions + ALTER COLUMN show_input SET NOT NULL; + +ALTER TABLE competitions + ALTER COLUMN show_output SET NOT NULL; \ No newline at end of file diff --git a/BACKEND_V2/src/main/resources/db/migrations/V3__users_role_as_table.sql b/BACKEND_V2/src/main/resources/db/migrations/V3__users_role_as_table.sql new file mode 100644 index 0000000..304ff2b --- /dev/null +++ b/BACKEND_V2/src/main/resources/db/migrations/V3__users_role_as_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE users_roles +( + user_id BIGINT NOT NULL, + role VARCHAR(255) +); + +ALTER TABLE users_roles + ADD CONSTRAINT fk_users_roles_on_user FOREIGN KEY (user_id) REFERENCES users_ (id); + +ALTER TABLE users_ + DROP COLUMN roles; \ No newline at end of file diff --git a/BACKEND_V2/src/test/kotlin/ru/codebattles/backend/BackendV2ApplicationTests.kt b/BACKEND_V2/src/test/kotlin/ru/codebattles/backend/BackendV2ApplicationTests.kt new file mode 100644 index 0000000..d39d41d --- /dev/null +++ b/BACKEND_V2/src/test/kotlin/ru/codebattles/backend/BackendV2ApplicationTests.kt @@ -0,0 +1,13 @@ +package ru.codebattles.backend + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class BackendV2ApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/FRONTEND/src/pages/teacher/LoginPage.jsx b/FRONTEND/src/pages/teacher/LoginPage.jsx index 5d54ae1..57ce38e 100644 --- a/FRONTEND/src/pages/teacher/LoginPage.jsx +++ b/FRONTEND/src/pages/teacher/LoginPage.jsx @@ -7,35 +7,35 @@ import If from "../../components/If"; const LoginPage = () => { const [login, setLogin] = useState(); const [passsword, setPasssword] = useState(); - const [captchaUserInput, setCaptchaUserInput] = useState(""); + // const [captchaUserInput, setCaptchaUserInput] = useState(""); const [errorMsg, setErrorMsg] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [captcha, setCaptcha] = useState({}) + // const [captcha, setCaptcha] = useState({}) const navigate = useNavigate(); - useEffect(() => { - apiAxios.get(getApiAddress() + '/api/teacher/auth') - .then((res) => { - setCaptcha(res.data) - }) - .catch(() => setErrorMsg("Неверные данные")) - .finally(() => setIsLoading(false)); - - }, [errorMsg]); + // useEffect(() => { + // apiAxios.get(getApiAddress() + '/api/teacher/auth') + // .then((res) => { + // setCaptcha(res.data) + // }) + // .catch(() => setErrorMsg("Неверные данные")) + // .finally(() => setIsLoading(false)); + // + // }, [errorMsg]); const onSend = async () => { setIsLoading(true) - await apiAxios.post(getApiAddress() + '/api/teacher/auth', + await apiAxios.post(getApiAddress() + '/api/auth/login', { login: login || "", password: passsword || "", - base64image: captcha.base64string, - captchaValidate: captcha.validate, - captchaUserInput: captchaUserInput.trim(), + // base64image: captcha.base64string, + // captchaValidate: captcha.validate, + // captchaUserInput: captchaUserInput.trim(), } ) .then( @@ -80,12 +80,12 @@ const LoginPage = () => { placeholder="Введите пароль" onChange={ (e) => setPasssword(e.target.value) }/> - - setCaptchaUserInput(e.target.value) - }/> + {/**/} + {/* setCaptchaUserInput(e.target.value)*/} + {/*}/>*/} diff --git a/FRONTEND_V2/.eslintrc.cjs b/FRONTEND_V2/.eslintrc.cjs deleted file mode 100644 index 6df9d7e..0000000 --- a/FRONTEND_V2/.eslintrc.cjs +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, - settings: { react: { version: '18.2' } }, - plugins: ['react-refresh'], - rules: { - 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} diff --git a/FRONTEND_V2/Dockerfile b/FRONTEND_V2/Dockerfile index 6ee9f3c..8214f8b 100644 --- a/FRONTEND_V2/Dockerfile +++ b/FRONTEND_V2/Dockerfile @@ -7,7 +7,8 @@ COPY package.json /app RUN yarn install COPY . /app -RUN yarn lint +# Disabled lint while developing +# RUN yarn lint RUN yarn build # production environment diff --git a/FRONTEND_V2/eslint.config.js b/FRONTEND_V2/eslint.config.js new file mode 100644 index 0000000..c481e5b --- /dev/null +++ b/FRONTEND_V2/eslint.config.js @@ -0,0 +1,20 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginReact from "eslint-plugin-react"; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { + files: ["src/**/*.{js,mjs,cjs,jsx}"], + languageOptions: { globals: globals.browser } + }, + pluginJs.configs.recommended, + pluginReact.configs.flat.recommended, + { + files: ["src/**/*.{js,mjs,cjs,jsx}"], + rules: { + "react/react-in-jsx-scope": "off", + "no-irregular-whitespace": "warn", + } + } +]; \ No newline at end of file diff --git a/FRONTEND_V2/package-lock.json b/FRONTEND_V2/package-lock.json index a006e58..91388ab 100644 --- a/FRONTEND_V2/package-lock.json +++ b/FRONTEND_V2/package-lock.json @@ -14,6 +14,7 @@ "bootstrap": "5.3.3", "bootstrap-icons": "^1.11.3", "dompurify": "^3.1.3", + "rc-select": "^14.16.6", "react": "^18.2.0", "react-ace": "^10.1.0", "react-dom": "^18.2.0", @@ -1089,6 +1090,43 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz", + "integrity": "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@remix-run/router": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", @@ -1390,9 +1428,9 @@ } }, "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, "node_modules/@types/prop-types": { "version": "15.7.12", @@ -1420,6 +1458,12 @@ "@types/react": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@types/unist": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", @@ -1922,6 +1966,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2179,9 +2228,12 @@ } }, "node_modules/dompurify": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", - "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/electron-to-chromium": { "version": "1.4.719", @@ -3104,15 +3156,15 @@ } }, "node_modules/hast-util-from-parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", - "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", - "hastscript": "^8.0.0", - "property-information": "^6.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" @@ -3157,14 +3209,14 @@ } }, "node_modules/hast-util-from-parse5/node_modules/hastscript": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", - "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" }, "funding": { @@ -3173,9 +3225,9 @@ } }, "node_modules/hast-util-from-parse5/node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3199,10 +3251,10 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-raw": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.4.tgz", - "integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==", +"node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", @@ -3237,9 +3289,9 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", - "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.5.tgz", + "integrity": "sha512-gHD+HoFxOMmmXLuq9f2dZDMQHVcplCVpMfBNRpJsF03yyLZvJGzsFORe8orVuYDX9k2w0VH0uF8oryFd1whqKQ==", "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", @@ -3251,7 +3303,7 @@ "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^1.0.0", "unist-util-position": "^5.0.0", @@ -3285,9 +3337,9 @@ } }, "node_modules/hast-util-to-jsx-runtime/node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3380,413 +3432,412 @@ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "engines": { - "node": "*" + "@types/unist": "*" } }, - "node_modules/html-url-attributes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz", - "integrity": "sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/hast-util-from-parse5/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "node_modules/hast-util-from-parse5/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-from-parse5/node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" + "@types/hast": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", - "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", - "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-from-parse5/node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" }, - "engines": { - "node": ">= 0.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "node_modules/hast-util-from-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, + "node_modules/hast-util-from-parse5/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-raw": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.4.tgz", + "integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==", "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-raw/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@types/unist": "*" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-raw/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", + "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, + "node_modules/hast-util-to-jsx-runtime/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-to-jsx-runtime/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-to-parse5/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-parse5/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/hast-util-to-parse5/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "dependencies": { - "call-bind": "^1.0.2" + "@types/hast": "^3.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "license": "MIT", + "node_modules/hast-util-whitespace/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@types/unist": "*" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", "dependencies": { - "is-extglob": "^2.1.1" + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 4" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number-object": { + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, + "node_modules/internal-slot": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -3795,12 +3846,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", "dev": true, "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3808,29 +3862,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" + "has-bigints": "^1.0.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, "license": "MIT", "dependencies": { + "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" }, "engines": { @@ -3840,15 +3892,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, "engines": { "node": ">= 0.4" }, @@ -3856,14 +3905,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -3872,12 +3934,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3885,10 +3950,29 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakref": { + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", "dev": true, "license": "MIT", "dependencies": { @@ -3898,15 +3982,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -3915,38 +3998,260 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { @@ -4102,60 +4407,977 @@ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", + "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz", + "integrity": "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/lowlight": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "micromark-util-types": "^2.0.0" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "yallist": "^3.0.2" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "dev": true, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/markdown-table": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", - "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", +"node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/mdast-util-find-and-replace": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", - "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", @@ -4179,9 +5401,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", - "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", @@ -4207,9 +5429,9 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, "node_modules/mdast-util-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", - "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", @@ -4241,9 +5463,9 @@ } }, "node_modules/mdast-util-gfm-footnote": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", - "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", @@ -4302,9 +5524,9 @@ } }, "node_modules/mdast-util-mdx-expression": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", - "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", @@ -4327,9 +5549,9 @@ } }, "node_modules/mdast-util-mdx-jsx": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz", - "integrity": "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", @@ -4362,15 +5584,6 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, - "node_modules/mdast-util-mdx-jsx/node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/mdast-util-mdx-jsx/node_modules/character-entities-legacy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", @@ -4430,12 +5643,11 @@ } }, "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", - "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", "dependencies": { "@types/unist": "^2.0.0", - "character-entities": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", @@ -4520,15 +5732,16 @@ } }, "node_modules/mdast-util-to-markdown": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", - "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" @@ -4556,9 +5769,9 @@ } }, "node_modules/micromark": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", - "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -4590,9 +5803,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", - "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -4693,9 +5906,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", - "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", @@ -4737,9 +5950,9 @@ } }, "node_modules/micromark-factory-destination": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", - "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", @@ -4757,9 +5970,9 @@ } }, "node_modules/micromark-factory-label": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", - "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", @@ -4778,9 +5991,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -4797,9 +6010,9 @@ } }, "node_modules/micromark-factory-title": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", - "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", @@ -4818,9 +6031,9 @@ } }, "node_modules/micromark-factory-whitespace": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", - "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -4839,9 +6052,9 @@ } }, "node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -4858,9 +6071,9 @@ } }, "node_modules/micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", @@ -4876,9 +6089,9 @@ } }, "node_modules/micromark-util-classify-character": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", - "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -4896,9 +6109,9 @@ } }, "node_modules/micromark-util-combine-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", - "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -4915,9 +6128,9 @@ } }, "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", - "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -4933,9 +6146,9 @@ } }, "node_modules/micromark-util-decode-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", - "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -4954,9 +6167,9 @@ } }, "node_modules/micromark-util-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", - "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -4969,9 +6182,9 @@ ] }, "node_modules/micromark-util-html-tag-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", - "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -4984,9 +6197,9 @@ ] }, "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", - "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -5002,9 +6215,9 @@ } }, "node_modules/micromark-util-resolve-all": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", - "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -5020,9 +6233,9 @@ } }, "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", - "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -5040,9 +6253,9 @@ } }, "node_modules/micromark-util-subtokenize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", - "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -5061,9 +6274,9 @@ } }, "node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -5076,9 +6289,9 @@ ] }, "node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -5369,11 +6582,11 @@ } }, "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", "dependencies": { - "entities": "^4.4.0" + "entities": "^4.5.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -5537,6 +6750,107 @@ ], "license": "MIT" }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz", + "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.6", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.6.tgz", + "integrity": "sha512-YPMtRPqfZWOm2XGTbx5/YVr1HT0vn//8QS77At0Gjb3Lv+Lbut0IORJPKLWu1hQ3u4GsA0SrDzs7nI8JG7Zmyg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/rc-virtual-list": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.18.3.tgz", + "integrity": "sha512-s1/bZQY2uwnmgXYeXxJkk2cSTz1cdUPDCrxAq/y1WQM115HilFFIvLi+JVFfkD4xCq3TZxGM17FQH4NLesWfwg==", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -5600,11 +6914,12 @@ "license": "MIT" }, "node_modules/react-markdown": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", - "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", "dependencies": { "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", @@ -5794,9 +7109,9 @@ } }, "node_modules/remark-gfm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", - "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", +"version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", @@ -5826,9 +7141,9 @@ } }, "node_modules/remark-rehype": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", - "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -5863,6 +7178,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -6278,11 +7598,11 @@ } }, "node_modules/style-to-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.7.tgz", - "integrity": "sha512-uSjr59G5u6fbxUfKbb8GcqMGT3Xs9v5IbPkjb0S16GyOeBLAzSRK0CixBv5YrYvzO6TDLzIS6QCn78tkqWngPw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", "dependencies": { - "inline-style-parser": "0.2.3" + "inline-style-parser": "0.2.4" } }, "node_modules/supports-color": { @@ -6646,9 +7966,9 @@ } }, "node_modules/usehooks-ts": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz", - "integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", + "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", "dependencies": { "lodash.debounce": "^4.0.8" }, @@ -6656,7 +7976,7 @@ "node": ">=16.15.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18" + "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "node_modules/vfile": { diff --git a/FRONTEND_V2/package.json b/FRONTEND_V2/package.json index 6d6f2c4..2766825 100644 --- a/FRONTEND_V2/package.json +++ b/FRONTEND_V2/package.json @@ -6,17 +6,19 @@ "scripts": { "dev": "vite", "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives", + "lint": "eslint --config eslint.config.js src/", + "lint:html": "eslint --config eslint.config.js src/ --format html > eslint-report.html", "preview": "vite preview" }, "dependencies": { "@popperjs/core": "^2.11.8", "ace-builds": "^1.32.9", - "axios": "^1.7.4", + "axios": "1.9.0", "bootstrap": "5.3.3", "bootstrap-icons": "^1.11.3", "dompurify": "^3.1.3", - "esbuild": "^0.24.0", + "prop-types": "^15.8.1", + "rc-select": "^14.16.6", "react": "^18.2.0", "react-ace": "^10.1.0", "react-dom": "^18.2.0", @@ -30,14 +32,16 @@ "usehooks-ts": "^3.1.0" }, "devDependencies": { + "@eslint/js": "^9.21.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.57.0", - "eslint-plugin-react": "^7.34.1", + "eslint": "^9.21.0", + "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", - "vite": "^5.4.6", + "globals": "^16.0.0", + "vite": "^5.2.0", "vite-plugin-chunk-split": "^0.5.0" } } diff --git a/FRONTEND_V2/src/App.jsx b/FRONTEND_V2/src/App.jsx index a24770a..334f5a2 100644 --- a/FRONTEND_V2/src/App.jsx +++ b/FRONTEND_V2/src/App.jsx @@ -1,18 +1,38 @@ -import SeeQuizzProblemPage from "./pages/SeeQuizzProblemPage.jsx"; - -import("../node_modules/bootstrap/dist/js/bootstrap.min.js") - - +import SeeQuizzProblemPage from "./pages/user/SeeQuizzProblemPage.jsx"; import Header from "./components/Header.jsx"; -import ProblemsListPage from "./pages/ProblemsListPage.jsx"; -import SendsListPage from "./pages/SendsListPage.jsx"; -import StatsPage from "./pages/StatsPage.jsx"; -import SeeProblemPage from "./pages/SeeProblemPage.jsx"; +import ProblemsListPage from "./pages/user/ProblemsListPage.jsx"; +import SendsListPage from "./pages/user/SendsListPage.jsx"; +import StatsPage from "./pages/user/StatsPage.jsx"; +import SeeProblemPage from "./pages/user/SeeProblemPage.jsx"; -import SeeSendPage from "./pages/SeeSendPage.jsx"; -import StatusesPage from "./pages/StatusesPage.jsx"; +import SeeSendPage from "./pages/user/SeeSendPage.jsx"; +import StatusesPage from "./pages/user/StatusesPage.jsx"; import {BrowserRouter, Route, Routes} from "react-router-dom"; -import LoginPage from "./pages/LoginPage.jsx"; +import LoginPage from "./pages/user/LoginPage.jsx"; +import ChampsPage from "./pages/user/ChampsPage.jsx"; +import {AdminChampsPage} from "./pages/champs/AdminChampsPage.jsx"; +import {AdminChampsDetailPage} from "./pages/champs/AdminChampsDetailPage.jsx"; +import {AdminProblemsPage} from "./pages/admin/problems/AdminProblemsPage.jsx"; +import {AdminCheckersPage} from "./pages/admin/checkers/AdminCheckersPage.jsx"; +import {AdminChampsDetailRatingPage} from "./pages/champs/AdminChampsDetailRatingPage.jsx"; +import {AdminSeeSendPage} from "./pages/admin/AdminSeeSendPage.jsx"; +import {NotFound} from "./pages/NotFound.jsx"; +import {AdminUsersDetailPage} from "./pages/admin/AdminUsersDetailPage.jsx"; +import {AdminChampsCreate} from "./pages/champs/AdminChampsCreate.jsx"; +import {AdminChampsDetailProblemsPage} from "./pages/champs/competitionProblems/AdminChampsDetailProblemsPage.jsx"; +import {AdminChampsDetailProblemsLinkPage} from "./pages/champs/competitionProblems/AdminChampsDetailProblemsLinkPage.jsx"; +import {AdminChampsDetailProblemsEditPage} from "./pages/champs/competitionProblems/AdminChampsDetailProblemsEditPage.jsx"; +import {AdminUsersDetailCheckersPage} from "./pages/admin/AdminUsersDetailCheckersPage.jsx"; +import {AdminCheckersEditPage} from "./pages/admin/checkers/AdminCheckersEditPage.jsx"; +import {AdminProblemsPageCreate} from "./pages/admin/problems/AdminProblemsPageCreate.jsx"; +import {AdminProblemsPageEdit} from "./pages/admin/problems/AdminProblemsPageEdit.jsx"; +import {AdminCheckersCreatePage} from "./pages/admin/checkers/AdminCheckersCreatePage.jsx"; +import {Profile} from "./pages/user/Profile.jsx"; +import {AdminUserCreatePage} from "./pages/admin/users/AdminUserCreatePage.jsx"; +import {AdminUsersPage} from "./pages/admin/users/AdminUsersPage.jsx"; + +import("../node_modules/bootstrap/dist/js/bootstrap.min.js") + function App() { @@ -25,13 +45,43 @@ function App() { }/> }/> - }/> - }/> + }/> + }/> }/> - }/> - }/> - }/> + }/> + }/> + }/> }/> + }/> + }/> + + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + } + /> + } + /> + }/> + }/> + }/> + + }/> + }/> + + }/> diff --git a/FRONTEND_V2/src/components/AdminHeader.jsx b/FRONTEND_V2/src/components/AdminHeader.jsx new file mode 100644 index 0000000..222a2d9 --- /dev/null +++ b/FRONTEND_V2/src/components/AdminHeader.jsx @@ -0,0 +1,15 @@ +import {Link} from "react-router-dom"; +import Card from "./bootstrap/Card.jsx"; + +export const AdminHeader = () => { + return ( + + соревнования + задачи + пользователи + чекеры + интерфейс ученика + + ); +}; + diff --git a/FRONTEND_V2/src/components/BreadcrumbsElement.jsx b/FRONTEND_V2/src/components/BreadcrumbsElement.jsx new file mode 100644 index 0000000..ebfd4af --- /dev/null +++ b/FRONTEND_V2/src/components/BreadcrumbsElement.jsx @@ -0,0 +1,34 @@ +import {Link} from "react-router-dom"; +import PropTypes from "prop-types"; + +const BreadcrumbsElement = ({name, url, active}) => { + + const isActiveClass = active ? ("active") : ("") + + return ( + + url ? ( +
  • + {name} +
  • + ) : ( +
  • + {name} +
  • + ) + + ); +}; + +BreadcrumbsElement.propTypes = { + name: PropTypes.string.isRequired, + url: PropTypes.string, + active: PropTypes.bool +}; + +BreadcrumbsElement.defaultProps = { + url: null, + active: false +}; + +export default BreadcrumbsElement; diff --git a/FRONTEND_V2/src/components/BreadcrumpsRoot.jsx b/FRONTEND_V2/src/components/BreadcrumpsRoot.jsx new file mode 100644 index 0000000..32be2f4 --- /dev/null +++ b/FRONTEND_V2/src/components/BreadcrumpsRoot.jsx @@ -0,0 +1,20 @@ +import Card from "./bootstrap/Card.jsx"; +import PropTypes from "prop-types"; + +const BreadcrumbsRoot = ({children}) => { + return ( + + + + ); +}; + +BreadcrumbsRoot.propTypes = { + children: PropTypes.node.isRequired +}; + +export default BreadcrumbsRoot; diff --git a/FRONTEND_V2/src/components/CompetitionCard.jsx b/FRONTEND_V2/src/components/CompetitionCard.jsx new file mode 100644 index 0000000..ea535e4 --- /dev/null +++ b/FRONTEND_V2/src/components/CompetitionCard.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Card from "./bootstrap/Card.jsx"; +import PropTypes from "prop-types"; + +export const CompetitionCard = ({name, description, children, id}) => { + return ( + +
    Идет
    +
    +

    {name}

    id={id || "0000"} +
    +

    {description}

    + + {children} +
    + ); +}; + +CompetitionCard.propTypes = { + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + children: PropTypes.node, + id: PropTypes.string, +} \ No newline at end of file diff --git a/FRONTEND_V2/src/components/DeleteButton.jsx b/FRONTEND_V2/src/components/DeleteButton.jsx new file mode 100644 index 0000000..3d4e89b --- /dev/null +++ b/FRONTEND_V2/src/components/DeleteButton.jsx @@ -0,0 +1,53 @@ +import React, {useState} from 'react'; +import constants from "../utils/consts.js"; +import axios from "axios"; +import {useNavigate} from "react-router-dom"; +import PropTypes from "prop-types"; + +export const DeleteButton = ({ + url, + }) => { + + const [disabled, setDisabled] = useState(false) + + const navigate = useNavigate() + + const conf = { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem(constants.LOCALSTORAGE_JWT)}` + } + } + + const onClick = () => { + setDisabled(true) + + axios.delete(url, conf) + .then(() => navigate("/admin/champs")) + .finally(() => setDisabled(false)) + } + + return ( + + ); +}; + +DeleteButton.propTypes = { + url: PropTypes.string.isRequired +}; \ No newline at end of file diff --git a/FRONTEND_V2/src/components/Header.jsx b/FRONTEND_V2/src/components/Header.jsx index 679fcfd..8c4a951 100644 --- a/FRONTEND_V2/src/components/Header.jsx +++ b/FRONTEND_V2/src/components/Header.jsx @@ -1,6 +1,9 @@ import "./css/Header.css" -import {Link, useNavigate} from "react-router-dom"; +import {Link, useLocation, useNavigate} from "react-router-dom"; import constants from "../utils/consts.js"; +import useCachedGetAPI from "../hooks/useGetAPI.js"; +import {useEffect} from "react"; + const Header = () => { @@ -8,11 +11,25 @@ const Header = () => { let isAuthed = localStorage.getItem(constants.LOCALSTORAGE_AUTH_KEY) === "true" + const [profile, update] = useCachedGetAPI("/api/users/me", () => { + }, {}); + + console.log(profile) + + const params = useLocation() + const compId = params.pathname.split("/")[2] + + useEffect(() => { + update() + }, []); + const onLogoutButtonClick = () => { localStorage.clear() navigate("/") } + // console.log("HEADER", params, needPath) + return (