diff --git a/.gitignore b/.gitignore
index f07d444..3ad247f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -107,6 +107,7 @@ celerybeat.pid
# Environments
.env
.venv
+.linvenv
env/
venv/
ENV/
@@ -147,5 +148,10 @@ cython_debug/
logs/
+yatb_prod
+
# env
yatb.env
+k3s.yaml
+dynamic_tasks_app/wtf
+yatb_state.json
diff --git a/app/__init__.py b/app/__init__.py
index 49bd99a..f2f74c4 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1,17 +1,32 @@
import logging
+from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
-from . import utils
+from . import api, main, utils, view
+from .api.api_dynamic_tasks import __client
from .config import settings
+from .db import beanie
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ await beanie.db.init()
+ async with __client:
+ try:
+ yield
+ finally:
+ await beanie.db.close()
+
app = FastAPI(
docs_url=settings.FASTAPI_DOCS_URL,
redoc_url=settings.FASTAPI_REDOC_URL,
openapi_url=settings.FASTAPI_OPENAPI_URL,
+ lifespan=lifespan,
)
_base_path = Path(__file__).resolve().parent
@@ -25,12 +40,10 @@
# for i in loggers:
# print(f"LOGGER: {i}")
-from . import api # noqa
-from . import main # noqa
-from . import view # noqa
-
+main.setup_utils(app)
app.include_router(api.router)
app.include_router(view.router)
+app.include_router(view.admin.api_rotuer)
# prometheus
from prometheus_fastapi_instrumentator import Instrumentator # noqa
@@ -43,15 +56,3 @@
env_var_name="ENABLE_METRICS",
)
instrumentator.instrument(app).expose(app, endpoint=expose_url)
-# utils.metrics.bad_solves_per_user
-
-"""
-@app.on_event("startup")
-def startup_event():
- metrics.load_all_metrics()
-
-
-@app.on_event("shutdown")
-def shutdown_event():
- metrics.save_all_metrics()
-"""
diff --git a/app/api/__init__.py b/app/api/__init__.py
index e14520d..281d4b0 100644
--- a/app/api/__init__.py
+++ b/app/api/__init__.py
@@ -13,8 +13,10 @@
from . import api_auth # noqa
from . import api_tasks # noqa
from . import api_users # noqa
+from . import api_dynamic_tasks # noqa
api_users.router.include_router(api_auth.router)
router.include_router(api_users.router)
router.include_router(api_tasks.router)
+router.include_router(api_dynamic_tasks.router)
router.include_router(admin.router)
diff --git a/app/api/admin/__init__.py b/app/api/admin/__init__.py
index 0cec0b3..0b1cd80 100644
--- a/app/api/admin/__init__.py
+++ b/app/api/admin/__init__.py
@@ -18,14 +18,14 @@ async def admin_checker(
user: auth.CURR_USER_SAFE,
token_header: str | None = Header(None, alias="X-Token"),
token_query: str | None = Query(None, alias="token"),
-) -> schema.User:
+) -> UserDB:
if user and user.is_admin:
return user
if token_header and token_header == settings.API_TOKEN:
- return _fake_admin_user
+ return _fake_admin_user # type: ignore
if token_query and token_query == settings.API_TOKEN:
- return _fake_admin_user
+ return _fake_admin_user # type: ignore
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -33,7 +33,7 @@ async def admin_checker(
)
-CURR_ADMIN = Annotated[schema.User, Depends(admin_checker)]
+CURR_ADMIN = Annotated[UserDB, Depends(admin_checker)]
logger = get_logger("api.admin")
router = APIRouter(
diff --git a/app/api/admin/admin_tasks.py b/app/api/admin/admin_tasks.py
index ef467ff..2d154ce 100644
--- a/app/api/admin/admin_tasks.py
+++ b/app/api/admin/admin_tasks.py
@@ -1,5 +1,6 @@
import uuid
-from typing import Annotated, Mapping
+from collections.abc import Mapping
+from typing import Annotated
from beanie import BulkWriter
from beanie.operators import Set
diff --git a/app/api/admin/admin_users.py b/app/api/admin/admin_users.py
index a58f230..75ece1f 100644
--- a/app/api/admin/admin_users.py
+++ b/app/api/admin/admin_users.py
@@ -16,7 +16,7 @@ async def api_admin_users_internal() -> Mapping[uuid.UUID, schema.User]:
return all_users
-async def api_admin_user_get_internal(user_id: uuid.UUID) -> UserDB:
+async def get_user(user_id: uuid.UUID) -> UserDB:
user = await UserDB.find_by_user_uuid(user_id)
if not user:
raise HTTPException(
@@ -27,29 +27,33 @@ async def api_admin_user_get_internal(user_id: uuid.UUID) -> UserDB:
return user
+CURR_USER = Annotated[UserDB, Depends(get_user)]
+
+
class PasswordChangeForm(BaseModel):
new_password: str
@router.get("/user/{user_id}")
-async def api_admin_user(user_id: uuid.UUID, user: CURR_ADMIN) -> schema.User.admin_model:
- ret_user = await api_admin_user_get_internal(user_id)
- return ret_user
+async def api_admin_user(admin: CURR_ADMIN, user: CURR_USER) -> schema.User.admin_model:
+ return user
@router.post("/user/{user_id}")
-async def api_admin_user_edit(new_user: schema.User, user_id: uuid.UUID, user: CURR_ADMIN) -> schema.User.admin_model:
- new_user = await db.update_user_admin(user_id, new_user)
+async def api_admin_user_edit(new_user: schema.User, user_id: uuid.UUID, admin: CURR_ADMIN) -> schema.User.admin_model:
+ # new_user = await db.update_user_admin(user_id, new_user)
+ raise Exception
+
return new_user
@router.get("/users/me")
-async def api_admin_users_me(user: CURR_ADMIN) -> schema.User.admin_model:
- return user
+async def api_admin_users_me(admin: CURR_ADMIN) -> schema.User.admin_model:
+ return admin
@router.get("/users")
-async def api_admin_users(user: CURR_ADMIN) -> Mapping[uuid.UUID, schema.User.admin_model]:
+async def api_admin_users(admin: CURR_ADMIN) -> Mapping[uuid.UUID, schema.User.admin_model]:
all_users = await api_admin_users_internal()
return all_users
@@ -58,7 +62,7 @@ async def api_admin_users(user: CURR_ADMIN) -> Mapping[uuid.UUID, schema.User.ad
async def api_admin_user_edit_password(
new_password: PasswordChangeForm,
admin: CURR_ADMIN,
- user: schema.User = Depends(api_admin_user_get_internal),
+ user: CURR_USER,
) -> schema.User.admin_model:
au = user.auth_source
if not isinstance(au, schema.auth.SimpleAuth.AuthModel):
@@ -73,7 +77,7 @@ async def api_admin_user_edit_password(
@router.get("/user/{user_id}/score")
async def api_admin_user_recalc_score(
admin: CURR_ADMIN,
- user: UserDB = Depends(api_admin_user_get_internal),
+ user: CURR_USER,
) -> schema.User.admin_model:
await user.recalc_score_one()
return user
@@ -82,18 +86,14 @@ async def api_admin_user_recalc_score(
@router.delete("/user/{user_id}")
async def api_admin_user_delete(
admin: CURR_ADMIN,
- user: schema.User = Depends(api_admin_user_get_internal),
+ user: CURR_USER,
) -> str:
- if not user:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail="user not exist",
- )
+ raise Exception
if len(user.solved_tasks) > 0:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="user have solved tasks",
)
- await db.delete_user(user)
+ # await db.delete_user(user)
return "deleted"
diff --git a/app/api/api_dynamic_tasks.py b/app/api/api_dynamic_tasks.py
new file mode 100644
index 0000000..5b8dd5c
--- /dev/null
+++ b/app/api/api_dynamic_tasks.py
@@ -0,0 +1,224 @@
+import datetime
+from collections.abc import Callable
+from ipaddress import ip_address
+from typing import Annotated, Literal, Self, TypeAlias, cast
+from uuid import UUID
+
+import httpx
+import humanize
+from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
+from fastapi.responses import HTMLResponse
+from httpx import AsyncClient
+from pydantic import BaseModel, TypeAdapter
+
+from .. import auth, db, schema
+from ..config import settings
+from ..db.beanie import TaskDB, UserDB
+from ..utils import metrics
+from ..utils.log_helper import get_logger
+from .api_tasks import CURRENT_TASK, get_task
+
+logger = get_logger("api.dynamic_tasks")
+
+router = APIRouter(
+ prefix="/dynamic",
+ tags=["dynamic_tasks"],
+)
+
+
+class DynamicTaskInfo(BaseModel):
+ name: str
+ descriptor: UUID
+
+ type: schema.task.DynamicTaskType
+
+ user_id: str
+
+ flag: str
+
+ @classmethod
+ def build(cls, task: schema.Task, user: schema.User) -> Self:
+ if not task.dynamic_task_info:
+ raise Exception("impossible")
+
+ return cls(
+ name=f"{task.task_id}",
+ descriptor=task.task_id,
+ type=task.dynamic_task_info.dynamic_task_type,
+ user_id=f"{user.user_id}",
+ flag=task.flag.flag_value(user),
+ )
+
+
+class ExternalDynamicTaskInfo(BaseModel):
+ class HostPortPair(BaseModel):
+ host: str
+ port: int
+
+ id: UUID
+
+ task_descriptor: UUID
+
+ user_id: str
+
+ hp: HostPortPair
+
+ least_time: datetime.timedelta
+
+
+class ExternalDynamicTaskError(BaseModel):
+ class Detail(BaseModel):
+ error: str
+
+ detail: Detail
+
+
+class MultipleInfoRequest(BaseModel):
+ tasks: list[UUID]
+
+
+class MultipleInfoResponse(BaseModel):
+ data: dict[UUID, str]
+
+
+_TT: TypeAlias = ExternalDynamicTaskInfo | ExternalDynamicTaskError
+ExternalDynamicTaskResp = TypeAdapter[_TT](_TT)
+
+
+class DynamicTasksClient(AsyncClient):
+ def __init__(self) -> None:
+ logger.info("Trying to start dynamic tasks client")
+
+ base_url = settings.DYNAMIC_TASKS_CONTROLLER or ""
+ x_token = settings.DYNAMIC_TASKS_CONTROLLER_TOKEN or ""
+
+ super().__init__(
+ base_url=base_url,
+ headers={
+ "X-Token": x_token,
+ },
+ timeout=httpx.Timeout(connect=5.0, read=120.0, write=5.0, pool=5.0),
+ )
+ logger.info(f"DTC startd with {base_url = } {x_token = }")
+
+ def format_resp(self, resp: httpx.Response) -> str:
+ info = ExternalDynamicTaskResp.validate_json(resp.text)
+ return self.format_info(info)
+
+ def format_info(self, info: _TT) -> str:
+ if not isinstance(info, ExternalDynamicTaskInfo):
+ return f"Status: {info.detail}"
+
+ ret = ""
+ ret += "Status: Running
"
+
+ try:
+ ip = ip_address(info.hp.host)
+
+ if ip.version == 4:
+ ip = f"{ip}"
+ elif ip.version == 6:
+ ip = f"[{ip}]"
+
+ except ValueError as ex:
+ ip = info.hp.host
+
+ link = f"http://{ip}:{info.hp.port}/"
+ ret += f"{link}
"
+
+ ret += f"Will die after {humanize.precisedelta(info.least_time)}"
+
+ return ret
+
+ async def start(self, task_info: DynamicTaskInfo) -> str:
+ resp = await self.post("/api/start", json=task_info.model_dump(mode="json"))
+ return self.format_resp(resp)
+
+ async def stop(self, task_info: DynamicTaskInfo):
+ resp = await self.post("/api/stop", json=task_info.model_dump(mode="json"))
+
+ if resp.status_code == status.HTTP_200_OK:
+ return "ok"
+
+ try:
+ err = ExternalDynamicTaskError.model_validate_json(resp.text)
+ except Exception as ex:
+ return "error?"
+ else:
+ return f"Status: {err.detail}"
+
+ async def restart(self, task_info: DynamicTaskInfo):
+ resp = await self.post("/api/restart", json=task_info.model_dump(mode="json"))
+ return self.format_resp(resp)
+
+ async def info(self, task_info: DynamicTaskInfo) -> str:
+ resp = await self.post("/api/info", json=task_info.model_dump(mode="json"))
+ return self.format_resp(resp)
+
+
+__client: DynamicTasksClient = DynamicTasksClient()
+
+
+async def __get_client() -> DynamicTasksClient:
+ if not settings.DYNAMIC_TASKS_CONTROLLER or not settings.DYNAMIC_TASKS_CONTROLLER_TOKEN:
+ raise HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail="dynamic tasks not enabled",
+ )
+ return __client
+
+
+async def get_dynamic_task(task: CURRENT_TASK) -> TaskDB:
+ if not task.dynamic_task_info:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Bad task",
+ )
+ return task
+
+
+CLIENT = Annotated[DynamicTasksClient, Depends(__get_client)]
+CURRENT_DYNAMIC_TASK = Annotated[TaskDB, Depends(get_dynamic_task)]
+
+
+@router.get("/start/{task_id}")
+async def api_dynamic_task_start(user: auth.CURR_USER, task: CURRENT_DYNAMIC_TASK, client: CLIENT) -> HTMLResponse:
+ info = await client.start(DynamicTaskInfo.build(task=task, user=user))
+ return HTMLResponse(info)
+
+
+@router.get("/stop/{task_id}")
+async def api_dynamic_task_stop(user: auth.CURR_USER, task: CURRENT_DYNAMIC_TASK, client: CLIENT) -> HTMLResponse:
+ info = await client.stop(DynamicTaskInfo.build(task=task, user=user))
+ return HTMLResponse(info)
+
+
+@router.get("/restart/{task_id}")
+async def api_dynamic_task_restart(user: auth.CURR_USER, task: CURRENT_DYNAMIC_TASK, client: CLIENT) -> HTMLResponse:
+ info = await client.restart(DynamicTaskInfo.build(task=task, user=user))
+ return HTMLResponse(info)
+
+
+@router.post("/infos")
+async def api_dynamic_task_infos(
+ user: auth.CURR_USER,
+ client: CLIENT,
+ req: MultipleInfoRequest,
+) -> MultipleInfoResponse:
+ resp = MultipleInfoResponse(data={})
+
+ for task_id in req.tasks:
+ try:
+ task = await get_dynamic_task(await get_task(task_id=task_id, user=user))
+ except HTTPException:
+ continue
+
+ resp.data[task_id] = await client.info(DynamicTaskInfo.build(task=task, user=user))
+
+ return resp
+
+
+@router.get("/info/{task_id}")
+async def api_dynamic_task_info(user: auth.CURR_USER, task: CURRENT_DYNAMIC_TASK, client: CLIENT) -> HTMLResponse:
+ info = await client.info(DynamicTaskInfo.build(task=task, user=user))
+ return HTMLResponse(info)
diff --git a/app/api/api_tasks.py b/app/api/api_tasks.py
index 31e534d..64a2d86 100644
--- a/app/api/api_tasks.py
+++ b/app/api/api_tasks.py
@@ -1,5 +1,6 @@
import uuid
from datetime import UTC, datetime
+from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
@@ -16,6 +17,19 @@
)
+async def get_task(task_id: uuid.UUID, user: auth.CURR_USER_SAFE) -> TaskDB:
+ task = await TaskDB.find_by_task_uuid(task_id)
+ if not task or not task.visible_for_user(user):
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="No task",
+ )
+ return task
+
+
+CURRENT_TASK = Annotated[TaskDB, Depends(get_task)]
+
+
@router.get("/")
async def api_tasks_get(user: auth.CURR_USER_SAFE) -> list[schema.Task.public_model]:
tasks = await TaskDB.get_all()
@@ -31,6 +45,11 @@ class BRMessage(schema.EBaseModel):
is_fb: bool
+@router.get("/{task_id}")
+async def api_task_get(task: CURRENT_TASK) -> schema.Task.public_model:
+ return task
+
+
@router.post("/submit_flag")
async def api_task_submit_flag(flag: schema.FlagForm, user: auth.CURR_USER) -> uuid.UUID:
if datetime.now(tz=UTC) < settings.EVENT_START_TIME:
@@ -103,14 +122,3 @@ async def api_task_submit_flag(flag: schema.FlagForm, user: auth.CURR_USER) -> u
logger.error(f"tg_exception exception='{ex}'")
return ret
-
-
-@router.get("/{task_id}")
-async def api_task_get(task_id: uuid.UUID, user: auth.CURR_USER_SAFE) -> schema.Task.public_model:
- task = await TaskDB.find_by_task_uuid(task_id)
- if not task or not task.visible_for_user(user):
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail="No task",
- )
- return task
diff --git a/app/api/api_users.py b/app/api/api_users.py
index a022a33..fa0faf9 100644
--- a/app/api/api_users.py
+++ b/app/api/api_users.py
@@ -1,6 +1,6 @@
import uuid
-from collections.abc import Sequence
-from typing import Iterable, TypeVar
+from collections.abc import Iterable, Sequence
+from typing import TypeVar
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
diff --git a/app/cli/base.py b/app/cli/base.py
index 8a215ed..c3fdc8f 100644
--- a/app/cli/base.py
+++ b/app/cli/base.py
@@ -1,16 +1,14 @@
import typer
-from pydantic_settings import BaseSettings
+from pydantic_settings import BaseSettings, SettingsConfigDict
from rich.console import Console
class Settings(BaseSettings):
- files_url: str = "http://127.0.0.1:9999"
- base_url: str = "http://127.0.0.1:8080"
-
- tasks_ip: str = "127.0.0.1"
-
- tasks_domain: str = "tasks.kksctf.ru"
- flag_base: str = "kks"
+ model_config = SettingsConfigDict(
+ env_file="yatb.env",
+ env_file_encoding="utf-8",
+ extra="allow",
+ )
settings = Settings()
diff --git a/app/cli/client.py b/app/cli/client.py
index 1ba0f81..baf9ed4 100644
--- a/app/cli/client.py
+++ b/app/cli/client.py
@@ -96,11 +96,21 @@ async def create_task(self, task: RawTask) -> schema.Task:
flag=schema.flags.StaticFlag(flag=task.flag, flag_base=settings.flag_base),
scoring=schema.scoring.DynamicKKSScoring(),
author=task.author,
+ dynamic_task_info=task.dynamic_task_type,
).model_dump(mode="json"),
)
).json()
return schema.Task.model_validate(new_task)
+ async def create_task_full_form(self, task: schema.TaskForm) -> schema.Task:
+ new_task = (
+ await self.s.post(
+ app.url_path_for("api_admin_task_create"),
+ json=task.model_dump(mode="json"),
+ )
+ ).json()
+ return schema.Task.model_validate(new_task)
+
async def admin_recalc_scoreboard(self) -> None:
resp = await self.s.get(app.url_path_for("api_admin_recalc_scoreboard"))
resp.raise_for_status()
diff --git a/app/cli/cmd/__init__.py b/app/cli/cmd/__init__.py
index f8dd9a2..5849aa0 100644
--- a/app/cli/cmd/__init__.py
+++ b/app/cli/cmd/__init__.py
@@ -16,9 +16,9 @@
from . import load as load_cmds
from . import stress as stress_cmds
-get_cmds = get_cmds
-stress_cmds = stress_cmds
-load_cmds = load_cmds
+get_cmds = get_cmds # noqa: PLW0127
+stress_cmds = stress_cmds # noqa: PLW0127
+load_cmds = load_cmds # noqa: PLW0127
tasks_to_create: list[RawTask] = [
RawTask(
diff --git a/app/cli/cmd/load.py b/app/cli/cmd/load.py
index 27d3ed5..394680f 100644
--- a/app/cli/cmd/load.py
+++ b/app/cli/cmd/load.py
@@ -1,4 +1,5 @@
import asyncio
+import json
import shutil
import subprocess
from pathlib import Path
@@ -6,61 +7,59 @@
from pydantic_yaml import parse_yaml_raw_as
-from ...schema.task import Task
+from ... import schema
+from ...schema.task import DynamicTaskInfo, DynamicTaskType, Task, TaskForm
from ..base import c, settings, tapp
from ..client import YATB
-from ..models import FileTask
+from ..models import FileTask, RawTask, State
+
+
+def task_to_raw(task: FileTask) -> TaskForm:
+ name = task.full_name
+ description = task.description.strip().strip('"').strip("'")
+
+ flag = task.flag
+ if flag.startswith(settings.flag_base + "{") and flag.endswith("}"):
+ flag = flag.removeprefix(settings.flag_base + "{")
+ flag = flag.removesuffix("}")
+
+ return TaskForm(
+ task_name=name,
+ category=task.category,
+ description=description,
+ author=task.author,
+ dynamic_task_info=DynamicTaskInfo(
+ dynamic_task_type=DynamicTaskType.SERVICE, # TODO: builder
+ ),
+ flag=schema.flags.DynamicKKSFlag(dynamic_flag_base=flag, flag_base=settings.flag_base),
+ scoring=schema.scoring.DynamicKKSScoring(),
+ )
@tapp.command()
def prepare_tasks(
main_tasks_dir: Path,
- static_files_dir: Path,
- deploy_files_dir: Path,
+ # static_files_dir: Path,
+ # deploy_files_dir: Path,
*,
drop: bool = False,
- live: bool = True,
+ # live: bool = True,
+ state_path: Path = Path() / "yatb_state.json",
):
+ state = State.model_validate_json(state_path.read_text()) if state_path.exists() else State()
+
main_tasks_dir = main_tasks_dir.expanduser().resolve()
- static_files_dir = static_files_dir.expanduser().resolve()
- deploy_files_dir = deploy_files_dir.expanduser().resolve()
async def _a():
- caddy_data = ""
- used_prefix: set[str] = set()
-
async with YATB() as y:
y.set_admin_token()
if drop:
- if static_files_dir.exists():
- c.log("Cleaning static files...")
- shutil.rmtree(static_files_dir)
-
- if deploy_files_dir.exists():
- c.log("Cleaning deploy files...")
- shutil.rmtree(deploy_files_dir)
-
await y.detele_everything()
+ state.task_to_uuid.clear()
- if live and deploy_files_dir.exists():
- c.log("Cleaning deploy files...")
- shutil.rmtree(deploy_files_dir)
-
- static_files_dir.mkdir(parents=True, exist_ok=True)
- deploy_files_dir.mkdir(parents=True, exist_ok=True)
-
- old_tasks: dict[UUID, Task] = {}
-
- def search_task(target: FileTask) -> Task | None:
- for task in old_tasks.values():
- if target.full_name == task.task_name:
- return task
- return None
-
- if live:
- old_tasks = await y.get_all_tasks()
- c.print(f"Running in live mode, found {len(old_tasks)} tasks")
+ tasks_cache: dict[UUID, Task] = await y.get_all_tasks()
+ c.print(f"Running in live mode, found {len(tasks_cache)} tasks")
for category_src in main_tasks_dir.iterdir():
if not category_src.is_dir():
@@ -73,74 +72,26 @@ def search_task(target: FileTask) -> Task | None:
if not (task_src / "task.yaml").exists():
continue
- task_info = parse_yaml_raw_as(FileTask, (task_src / "task.yaml").read_text())
-
- board_task = search_task(task_info)
- if live and not board_task:
- c.print(f"WTF: {task_info = } not found")
- input("wtf?")
-
- created_task = board_task or await y.create_task(task_info.get_raw())
- c.print(f"Created task: {created_task}")
-
- public_dir = task_src / "public"
- if public_dir.exists() and not live:
- task_files_dir = static_files_dir / str(created_task.task_id)
- task_files_dir.mkdir(parents=True, exist_ok=True)
-
- files = list(public_dir.iterdir())
- files_hash = subprocess.check_output( # noqa: ASYNC101
- "sha256sum -b public/*", # noqa: S607
- shell=True, # noqa: S602
- cwd=task_src,
- stderr=subprocess.STDOUT,
- )
-
- created_task.description += "\n\n---\n\n"
- created_task.description += '
'
-
- for file in files:
- created_task.description += (
- f"
{file.name}\n"
- )
- shutil.copy2(file, task_files_dir)
- c.print(f"\t\t[+] '{created_task.task_name}': uploaded file {file}")
-
- (task_files_dir / ".sha256").write_bytes(files_hash)
- created_task.description += (
- f"
.sha256\n"
- )
-
- created_task.description = created_task.description.strip() + "
"
-
- created_task = await y.update_task(task=created_task)
-
- c.print(f"Updated task: {created_task}")
-
- if (
- task_info.server_port
- and task_info.is_http
- and (prefix := task_info.domain_prefix)
- and prefix not in used_prefix
- ):
- caddy_data += (
- f"@task-{prefix} host {prefix}.{settings.tasks_domain}\n"
- f"handle @task-{prefix} {{\n"
- f" reverse_proxy 127.0.0.1:{task_info.server_port}\n"
- "}\n\n"
- )
- used_prefix.add(prefix)
-
- deploy_dir = task_src / "deploy"
- if deploy_dir.exists():
- target_deploy_dir = deploy_files_dir / task_src.name
- shutil.copytree(deploy_dir, target_deploy_dir)
-
- c.print("Caddy data:")
- c.print(caddy_data)
-
- asyncio.run(_a())
+ try:
+ task_info = parse_yaml_raw_as(FileTask, (task_src / "task.yaml").read_text())
+ except Exception as ex:
+ c.print(f"ERROR!!! {task_src = } has bad yaml: {ex!r}")
+ continue
+
+ if task_src not in state.task_to_uuid:
+ created_task = await y.create_task_full_form(task_to_raw(task_info))
+ tasks_cache[created_task.task_id] = created_task
+ state.task_to_uuid[task_src] = created_task.task_id
+ c.print(f"Created task: {created_task}")
+ continue
+
+ created_task = tasks_cache[state.task_to_uuid[task_src]]
+ c.print(f"Found task: {created_task}")
+
+ to_env = {str(uuid): str((path / "deploy").resolve()) for path, uuid in state.task_to_uuid.items()}
+ c.print(json.dumps(to_env, indent=4))
+
+ try:
+ asyncio.run(_a())
+ finally:
+ state_path.write_text(state.model_dump_json(indent=4))
diff --git a/app/cli/models.py b/app/cli/models.py
index 5236238..1ae5ebd 100644
--- a/app/cli/models.py
+++ b/app/cli/models.py
@@ -3,6 +3,7 @@
import uuid
from dataclasses import dataclass
from datetime import datetime
+from pathlib import Path
from pydantic import BaseModel, RootModel
@@ -41,6 +42,8 @@ class RawTask:
author: str = ""
+ dynamic_task_type: schema.DynamicTaskInfo | None = None
+
class FileTask(BaseModel):
name: str
@@ -50,9 +53,7 @@ class FileTask(BaseModel):
category: str
flag: str
- is_gulag: bool = False
- warmup: bool = False
server_port: int | None = None
is_http: bool = True
@@ -62,45 +63,9 @@ class FileTask(BaseModel):
def full_name(self) -> str:
return self.name
- def get_raw(self) -> RawTask:
- name = self.full_name
-
- description = self.description.strip().strip('"').strip("'")
-
- if self.server_port:
- if self.is_http:
- if self.domain_prefix:
- server_addr = f"https://{self.domain_prefix}.{settings.tasks_domain}/"
- else:
- server_addr = f"http://{settings.tasks_ip}:{self.server_port}"
-
- description += "\n\n---\n\n"
- description += ''
- description += (
- f"
{server_addr}\n"
- )
- description += "
"
- else:
- description += (
- "\n\n---\n\n"
- f"`nc {settings.tasks_ip} {self.server_port}`"
- "\n" #
- )
-
- flag = self.flag
- if flag.startswith(settings.flag_base + "{") and flag.endswith("}"):
- flag = flag.removeprefix(settings.flag_base + "{")
- flag = flag.removesuffix("}")
-
- return RawTask(
- task_name=name,
- category=self.category,
- description=description,
- flag=flag,
- author=self.author,
- )
+
+class State(BaseModel):
+ task_to_uuid: dict[Path, uuid.UUID] = {}
AllUsers = RootModel[dict[uuid.UUID, UserPrivate]]
diff --git a/app/config.py b/app/config.py
index cb3d7f7..d0414b4 100644
--- a/app/config.py
+++ b/app/config.py
@@ -15,6 +15,8 @@ class DefaultTokenError(ValueError):
class Settings(BaseSettings):
DEBUG: bool = False
+ TESTING: bool = False
+
PROFILING: bool = False
TOKEN_PATH: str = "/api/users/login"
@@ -63,9 +65,12 @@ class Settings(BaseSettings):
"DiscordOAuth",
]
+ DYNAMIC_TASKS_CONTROLLER: str | None = None
+ DYNAMIC_TASKS_CONTROLLER_TOKEN: str | None = None
+
@model_validator(mode="after")
def check_non_default_tokens(self) -> Self:
- if self.DEBUG:
+ if self.DEBUG or self.TESTING:
return self
token_check_list = ["JWT_SECRET_KEY", "FLAG_SIGN_KEY", "API_TOKEN", "WS_API_TOKEN"]
diff --git a/app/db/__init__.py b/app/db/__init__.py
index 8ba57f2..e69de29 100644
--- a/app/db/__init__.py
+++ b/app/db/__init__.py
@@ -1,115 +0,0 @@
-import os
-import pickle
-from datetime import datetime
-
-from pydantic import BaseModel
-
-from .. import app, db, schema
-from ..config import settings
-from ..utils.log_helper import get_logger
-from .beanie import TaskDB, UserDB
-
-logger = get_logger("db")
-
-
-class FileDB:
- _db = None
- _index = None
-
- def __init__(self):
- self.reset_db()
-
- def reset_db(self):
- self._index = {
- "tasks": {},
- "users": {},
- "short_urls": {},
- }
- self._db = {
- "tasks": {},
- "users": {},
- }
-
- def generate_index(self):
- for i, v in self._db["tasks"].items():
- self._index["tasks"][v.task_id] = self.update_task(v)
-
- for i, v in self._db["users"].items():
- self._index["users"][v.user_id] = self.update_user(v)
-
- def update_task(self, task: schema.Task):
- # regenerate markdown
- task.description_html = schema.Task.regenerate_md(task.description)
-
- return task
-
- def update_user(self, user: schema.User):
- # FIXME: говнокод & быстрофикс.
- if isinstance(user.auth_source, dict):
- original_au = user.auth_source
- cls: schema.auth.AuthBase.AuthModel = getattr(schema.auth, user.auth_source["classtype"]).AuthModel
- user.auth_source = cls.model_validate(user.auth_source)
- logger.warning(f"Found & fixed broken auth source: {original_au} -> {user.auth_source}")
-
- # admin promote
- if user.admin_checker() and not user.is_admin:
- logger.warning(f"INIT: Promoting {user} to admin")
- user.is_admin = True
-
- return user
-
-
-_db = FileDB()
-
-
-@app.on_event("startup")
-async def startup_event():
- return
- global _db
- if settings.DB_NAME is None:
- _db.reset_db()
- logger.warning("TESTING_FileDB loaded")
- return
-
- if not os.path.exists(settings.DB_NAME):
- _db._db = {
- "tasks": {},
- "users": {},
- }
- else:
- try:
- with open(settings.DB_NAME, "rb") as f:
- _db._db = pickle.load(f)
- except Exception as ex:
- _db._db = {
- "tasks": {},
- "users": {},
- }
- logger.error(f"Loading db exception, fallback to empty, {ex}")
-
- _db.generate_index()
- logger.warning("FileDB loaded")
- # logger.debug(f"FileDB: {_db._db}")
- # logger.debug(f"FileDBIndex: {_db._index}")
-
-
-@app.on_event("shutdown")
-async def shutdown_event():
- return
- global _db
- if settings.DB_NAME is None:
- return
- save_path = settings.DB_NAME / "ressurect_db.db" if settings.DB_NAME.is_dir() else settings.DB_NAME
- with open(settings.DB_NAME, "wb") as f:
- pickle.dump(_db._db, f)
- logger.warning("FileDB saved")
-
-
-def update_entry(obj: BaseModel, data: dict):
- for i in data:
- if i in obj.__fields__:
- setattr(obj, i, data[i])
-
-
-# from .db_tasks import * # noqa
-# from .db_users import * # noqa
diff --git a/app/db/beanie.py b/app/db/beanie.py
index 2b1785e..7a25b17 100644
--- a/app/db/beanie.py
+++ b/app/db/beanie.py
@@ -1,19 +1,16 @@
import datetime
import uuid
from collections.abc import Hashable, Mapping
-from typing import Annotated, Any, ClassVar, Generic, Literal, Self, TypeVar, final
+from typing import Any, ClassVar, Generic, Literal, Self, TypeVar, final
-import bson
import pymongo
from beanie import BulkWriter, Document, init_beanie
from beanie.operators import And as _And
from beanie.operators import Set
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
-from pydantic import PlainSerializer
-from .. import app
from ..config import settings
-from ..schema import EBaseModel, Task, TaskForm, User, auth
+from ..schema import EBaseModel, FlagCheckResult, Task, TaskForm, User, auth
from ..schema.ebasemodel import FilterFieldsType
from ..utils.log_helper import get_logger
@@ -83,16 +80,8 @@ async def update_entry(self, new_task: Task) -> Self:
return self
@classmethod
- async def populate(cls: type[Self], new_task: TaskForm, author: User) -> Self:
- task = cls(
- task_name=new_task.task_name,
- category=new_task.category,
- scoring=new_task.scoring,
- description=new_task.description,
- description_html=Task.regenerate_md(new_task.description),
- flag=new_task.flag,
- author=(new_task.author if new_task.author != "" else f"@{author.username}"),
- )
+ async def populate(cls, new_task: TaskForm, author: User) -> Self:
+ task = new_task.to_task(cls, author)
await task.insert() # type: ignore # WTF: bad library
return task
@@ -107,9 +96,15 @@ async def get_all(cls: type[Self]) -> dict[uuid.UUID, Self]:
@classmethod
async def find_by_flag(cls: type[Self], flag: str, user: User) -> Self | None:
for task in await cls.find_all().to_list():
- if task.flag.flag_checker(flag, user):
+ result = task.flag.flag_checker(flag, user)
+ if result == FlagCheckResult.valid:
return task
+ if result == FlagCheckResult.invalid:
+ continue
+
+ logger.warning(f"user=[{user.short_desc()}], task=[{task.short_desc()}], {flag=}, {result.name=}")
+
return None
@classmethod
@@ -311,34 +306,18 @@ def __init__(self) -> None:
async def init(self) -> None:
self.client = AsyncIOMotorClient(str(settings.MONGO), tz_aware=True)
self.db = self.client[settings.DB_NAME]
- await init_beanie(database=self.db, document_models=[TaskDB, UserDB]) # type: ignore # bad library ;(
+ await init_beanie(database=self.db, document_models=[TaskDB, UserDB])
logger.info("Beanie init ok")
async def close(self) -> None:
logger.info("DB close ok")
async def reset_db(self) -> None:
- if not settings.DEBUG:
- logger.warning("DB Reset without debug")
+ if not (settings.DEBUG or settings.TESTING):
+ logger.warning(f"DB Reset without debug ({settings.DEBUG = }) or testing {settings.TESTING = }")
return
await self.client.drop_database(settings.DB_NAME)
-@app.on_event("startup")
-async def startup_event():
- await db.init()
-
-
-@app.on_event("shutdown")
-async def shutdown_event():
- await db.close()
-
-
-# async def init_db():
-# await db.init()
-# yield
-# await db.close()
-
-
db = DBClient()
diff --git a/app/db/db_tasks.py b/app/db/db_tasks.py
deleted file mode 100644
index cc62900..0000000
--- a/app/db/db_tasks.py
+++ /dev/null
@@ -1,174 +0,0 @@
-import datetime
-import logging
-import uuid
-from asyncio import Lock
-from typing import Dict, Union
-
-from .. import schema
-from ..config import settings
-from ..utils import metrics
-from ..utils.log_helper import get_logger
-from . import db_users, update_entry
-
-# import markdown2
-
-
-logger = get_logger("db.tasks")
-db_lock = Lock()
-
-
-async def get_task_uuid(uuid: uuid.UUID) -> schema.Task:
- from . import _db
-
- if uuid in _db._index["tasks"]:
- return _db._index["tasks"][uuid]
-
-
-async def get_all_tasks() -> Dict[uuid.UUID, schema.Task]:
- from . import _db
-
- return _db._index["tasks"]
-
-
-async def check_task_uuid(uuid: uuid.UUID) -> bool:
- from . import _db
-
- return uuid in _db._index["tasks"]
-
-
-async def insert_task(new_task: schema.TaskForm, author: schema.User) -> schema.Task:
- from . import _db
-
- # task = schema.Task.parse_obj(new_task) # WTF: SHITCODE
- task = schema.Task(
- task_name=new_task.task_name,
- category=new_task.category,
- scoring=new_task.scoring,
- description=new_task.description,
- description_html=schema.Task.regenerate_md(new_task.description),
- flag=new_task.flag,
- author=(new_task.author if new_task.author != "" else f"@{author.username}"),
- )
-
- _db._db["tasks"][task.task_id] = task
- _db._index["tasks"][task.task_id] = task
- return task
-
-
-async def update_task(task: schema.Task, new_task: schema.Task) -> schema.Task:
- from . import _db
-
- logger.debug(f"Update task {task} to {new_task}")
-
- update_entry(
- task,
- new_task.dict(
- exclude={
- "task_id",
- "description_html",
- "scoring",
- "flag",
- "pwned_by",
- }
- ),
- )
- task.scoring = new_task.scoring # fix for json-ing scoring on edit
- task.flag = new_task.flag # fix for json-ing flag on edit
-
- logger.debug(f"Resulting task={task}")
- task.description_html = schema.Task.regenerate_md(task.description)
- return task
-
-
-async def remove_task(task: schema.Task):
- from . import _db
-
- # TODO: recalc score and something else.
- await unsolve_task(task)
- del _db._db["tasks"][task.task_id]
- del _db._index["tasks"][task.task_id]
-
-
-async def find_task_by_flag(flag: str, user: schema.User) -> Union[schema.Task, None]:
- from . import _db
-
- for task_id, task in _db._db["tasks"].items():
- task: schema.Task # strange solution, but no other ideas
- if task.flag.flag_checker(flag, user):
- return task
-
- return None
-
-
-async def solve_task(task: schema.Task, solver: schema.User):
- if solver.is_admin and not settings.DEBUG: # if you admin, you can't solve task.
- return task.task_id
-
- if datetime.datetime.now(tz=datetime.UTC) > settings.EVENT_END_TIME:
- return task.task_id
-
- # WTF: UNTEDTED: i belive this will work as a monkey patch for rAcE c0nDiTioN
- global db_lock
- async with db_lock:
- # add references
- solv_time = datetime.datetime.now(tz=datetime.UTC)
- solver.solved_tasks[task.task_id] = solv_time
- task.pwned_by[solver.user_id] = solv_time
-
- # get previous score
- prev_score = task.scoring.points
- solver.score += prev_score
-
- # if do_recalc, recalc all the scoreboard... only users, who solved task
- do_recalc = task.scoring.solve_task()
- if do_recalc:
- new_score = task.scoring.points
- diff = prev_score - new_score
- logger.info(f"Solve task: {task.short_desc()}, oldscore={prev_score}, newscore={new_score}, diff={diff}")
- for solver_id in task.pwned_by:
- solver_recalc = await db_users.get_user_uuid(solver_id)
- solver_recalc.score -= diff
- metrics.score_per_user.labels(user_id=solver_recalc.user_id, username=solver_recalc.username).set(
- solver_recalc.score
- )
-
- return task.task_id
-
-
-async def unsolve_task(task: schema.Task) -> schema.Task:
- # add references
- global db_lock
- async with db_lock:
- task.pwned_by.clear()
- # TODO: оптимизировать эту ебатеку
- for _, user in (await db_users.get_all_users()).items():
- if task.task_id in user.solved_tasks:
- user.solved_tasks.pop(task.task_id)
- # task.scoring
-
- await recalc_scoreboard()
- return task
-
-
-async def recalc_user_score(user: schema.User, _task_cache: Dict[uuid.UUID, schema.Task] = None):
- if _task_cache is None:
- _task_cache = {}
- old_score = user.score
- user.score = 0
- for task_id in user.solved_tasks:
- if task_id not in _task_cache:
- _task_cache[task_id] = await get_task_uuid(task_id)
- if _task_cache[task_id] is None:
- continue
- user.score += _task_cache[task_id].scoring.points
- _task_cache[task_id].scoring.set_solves(len(_task_cache[task_id].pwned_by))
- if old_score != user.score:
- logger.warning(f"Recalc: smth wrong with {user.short_desc()}, {old_score} != {user.score}!")
-
-
-async def recalc_scoreboard():
- _task_cache: Dict[uuid.UUID, schema.Task] = {}
- global db_lock
- async with db_lock:
- for _, user in (await db_users.get_all_users()).items():
- await recalc_user_score(user, _task_cache)
diff --git a/app/db/db_users.py b/app/db/db_users.py
deleted file mode 100644
index e58347a..0000000
--- a/app/db/db_users.py
+++ /dev/null
@@ -1,129 +0,0 @@
-from app.db import update_entry
-import uuid
-import logging
-from typing import Hashable, List, Dict, Optional, Type
-
-from .. import schema
-from ..utils.log_helper import get_logger
-
-logger = get_logger("db.users")
-
-# logger.debug(f"GlobalUsers, FileDB: {_db}")
-
-
-async def get_user(username: str) -> Optional[schema.User]:
- from . import _db
-
- for i in _db._index["users"]:
- if _db._index["users"][i].username == username:
- return _db._index["users"][i]
- return None
-
-
-async def get_user_uuid(uuid: uuid.UUID) -> Optional[schema.User]:
- from . import _db
-
- if uuid in _db._index["users"]:
- return _db._index["users"][uuid]
-
-
-async def get_user_uniq_field(base: Type[schema.auth.AuthBase.AuthModel], field: Hashable) -> schema.User:
- from . import _db
-
- for i in _db._index["users"]:
- if (
- type(_db._index["users"][i].auth_source) == base
- and _db._index["users"][i].auth_source.get_uniq_field() == field
- ):
- return _db._index["users"][i]
- return None
-
-
-async def get_all_users() -> Dict[uuid.UUID, schema.User]:
- from . import _db
-
- return _db._db["users"]
-
-
-async def check_user(username: str) -> bool:
- from . import _db
-
- for i in _db._index["users"]:
- if _db._index["users"][i].username == username:
- return True
- return False
-
-
-async def check_user_uuid(uuid: uuid.UUID) -> bool:
- from . import _db
-
- return uuid in _db._index["users"]
-
-
-async def check_user_uniq_field(base: Type[schema.auth.AuthBase.AuthModel], field: Hashable) -> bool:
- from . import _db
-
- for i in _db._index["users"]:
- if (
- type(_db._index["users"][i].auth_source) == base
- and _db._index["users"][i].auth_source.get_uniq_field() == field
- ):
- return True
- return False
-
-
-async def insert_user(auth: schema.auth.TYPING_AUTH):
- from . import _db
-
- # WTF: SHITCODE or not.... :thonk:
- user = schema.User(auth_source=auth)
- _db._db["users"][user.user_id] = user
- _db._index["users"][user.user_id] = user
- return user
-
-
-"""
-async def insert_oauth_user(oauth_id: int, username: str, country: str):
- from . import _db
-
- # WTF: SHITCODE
- user = schema.User(
- username=username,
- password_hash=None,
- country=country,
- oauth_id=oauth_id,
- )
- _db._db["users"][user.user_id] = user
- _db._index["users"][user.user_id] = user
- return user
-"""
-
-
-async def update_user_admin(user_id: uuid.UUID, new_user: schema.User):
- from . import _db
-
- user: schema.User = _db._index["users"][user_id]
- logger.debug(f"Update user {user} to {new_user}")
-
- update_entry(
- user,
- new_user.dict(
- exclude={
- "user_id",
- "password_hash",
- "score",
- "solved_tasks",
- "oauth_id",
- }
- ),
- )
- # user.parse_obj(new_user)
- logger.debug(f"Resulting user={user}")
- return user
-
-
-async def delete_user(user: schema.User):
- from . import _db
-
- del _db._db["users"][user.user_id]
- del _db._index["users"][user.user_id]
diff --git a/app/main.py b/app/main.py
index 61bd116..89b24e6 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,68 +1,73 @@
-from . import app, root_logger
-from .api import api_tasks
+import time
+
+from fastapi import FastAPI, Request
+
from .config import settings
+from .utils.log_helper import root_logger
+
-"""
-@app.middleware("http")
-async def session_middleware(request: Request, call_next):
- # start_time = time.time()
+async def simple_timing_middleware(request: Request, call_next):
+ start_time = time.time()
response = await call_next(request)
- # process_time = time.time() - start_time
- # response.headers["X-Process-Time"] = str(process_time)
+ process_time = time.time() - start_time
+ response.headers["X-Process-Time"] = str(process_time)
return response
-"""
-if settings.DEBUG:
- try:
- import fastapi # noqa
- import pydantic # noqa
- from asgi_server_timing import ServerTimingMiddleware # noqa # type: ignore
- root_logger.warning("Timing debug loading")
+def setup_utils(app: FastAPI):
+ if settings.DEBUG:
+ app.middleware("http")(simple_timing_middleware)
+
+ try:
+ import fastapi # noqa
+ import pydantic # noqa
+ from asgi_server_timing import ServerTimingMiddleware # noqa # type: ignore
+
+ root_logger.warning("Timing debug loading")
- app.add_middleware(
- ServerTimingMiddleware,
- calls_to_track={
- "1deps": (fastapi.routing.solve_dependencies,), # type: ignore
- "2main": (fastapi.routing.run_endpoint_function,),
- # "3valid": (pydantic.fields.ModelField.validate,),
- "4encode": (fastapi.encoders.jsonable_encoder,), # type: ignore
- "5render": (
- fastapi.responses.JSONResponse.render,
- fastapi.responses.ORJSONResponse.render,
- fastapi.responses.HTMLResponse.render,
- fastapi.responses.PlainTextResponse.render,
- ),
- # "6tasks": (api_tasks.api_tasks_get,),
- # "6task": (api_tasks.api_task_get,),
- },
- )
- root_logger.warning("Timing debug loaded")
+ app.add_middleware(
+ ServerTimingMiddleware,
+ calls_to_track={
+ "1deps": (fastapi.routing.solve_dependencies,), # type: ignore
+ "2main": (fastapi.routing.run_endpoint_function,),
+ # "3valid": (pydantic.fields.ModelField.validate,),
+ "4encode": (fastapi.encoders.jsonable_encoder,), # type: ignore
+ "5render": (
+ fastapi.responses.JSONResponse.render,
+ fastapi.responses.ORJSONResponse.render,
+ fastapi.responses.HTMLResponse.render,
+ fastapi.responses.PlainTextResponse.render,
+ ),
+ # "6tasks": (api_tasks.api_tasks_get,),
+ # "6task": (api_tasks.api_task_get,),
+ },
+ )
+ root_logger.warning("Timing debug loaded")
- except ModuleNotFoundError:
- root_logger.warning("No timing extensions found")
+ except ModuleNotFoundError:
+ root_logger.warning("No timing extensions found")
-if settings.PROFILING:
- try:
- from fastapi import Request
- from fastapi.responses import HTMLResponse
- from pyinstrument import Profiler
+ if settings.PROFILING:
+ try:
+ from fastapi import Request
+ from fastapi.responses import HTMLResponse
+ from pyinstrument import Profiler
- root_logger.warning("pyinstrument loading")
+ root_logger.warning("pyinstrument loading")
- @app.middleware("http")
- async def profile_request(request: Request, call_next):
- profiling = request.query_params.get("profile", False)
- if profiling:
- profiler = Profiler(async_mode="enabled") # interval=settings.profiling_interval
- profiler.start()
- await call_next(request)
- profiler.stop()
- return HTMLResponse(profiler.output_html())
- else:
- return await call_next(request)
+ @app.middleware("http")
+ async def profile_request(request: Request, call_next):
+ profiling = request.query_params.get("profile", False)
+ if profiling:
+ profiler = Profiler(async_mode="enabled") # interval=settings.profiling_interval
+ profiler.start()
+ await call_next(request)
+ profiler.stop()
+ return HTMLResponse(profiler.output_html())
+ else:
+ return await call_next(request)
- root_logger.warning("pyinstrument loaded")
+ root_logger.warning("pyinstrument loaded")
- except ModuleNotFoundError:
- root_logger.warning("No pyinstrument found")
+ except ModuleNotFoundError:
+ root_logger.warning("No pyinstrument found")
diff --git a/app/schema/__init__.py b/app/schema/__init__.py
index 419aa71..0b3479d 100644
--- a/app/schema/__init__.py
+++ b/app/schema/__init__.py
@@ -1,9 +1,9 @@
from ..utils.log_helper import get_logger
from .auth import AuthBase, CTFTimeOAuth, OAuth, SimpleAuth, TelegramAuth
from .ebasemodel import EBaseModel
-from .flags import DynamicKKSFlag, Flag, StaticFlag
+from .flags import DynamicKKSFlag, Flag, FlagCheckResult, StaticFlag
from .scoring import DynamicKKSScoring, Scoring, StaticScoring
-from .task import FlagUnion, ScoringUnion, Task, TaskForm
+from .task import DynamicTaskInfo, DynamicTaskType, FlagUnion, ScoringUnion, Task, TaskForm
from .user import User
logger = get_logger("schema")
diff --git a/app/schema/auth/simple.py b/app/schema/auth/simple.py
index f3b7271..c76ca44 100644
--- a/app/schema/auth/simple.py
+++ b/app/schema/auth/simple.py
@@ -63,12 +63,13 @@ def check_password(self, model: "SimpleAuth.AuthModel") -> bool:
def check_valid(self) -> bool:
if settings.DEBUG:
return True
+
if (
len(self.internal.username) < SimpleAuth.auth_settings.MIN_USERNAME_LEN
or len(self.internal.username) > SimpleAuth.auth_settings.MAX_USERNAME_LEN
):
return False
- if len(self.internal.password) < SimpleAuth.auth_settings.MIN_PASSWORD_LEN:
+ if len(self.internal.password) < SimpleAuth.auth_settings.MIN_PASSWORD_LEN: # noqa: SIM103
return False
return True
diff --git a/app/schema/ebasemodel.py b/app/schema/ebasemodel.py
index b90d371..0204ffe 100644
--- a/app/schema/ebasemodel.py
+++ b/app/schema/ebasemodel.py
@@ -62,7 +62,7 @@ def build_model( # noqa: PLR0912, C901 # WTF: refactor & simplify
new_union = Union[tuple(new_union_base)] # type: ignore # noqa: UP007, PGH003 # так надо.
- target_fields[field_name] = (
+ target_fields[field_name] = ( # type: ignore # WTF: ???
new_union,
field_value,
)
diff --git a/app/schema/flags.py b/app/schema/flags.py
index f4d2e2d..914b7c2 100644
--- a/app/schema/flags.py
+++ b/app/schema/flags.py
@@ -1,5 +1,6 @@
import binascii
import hmac
+from enum import Enum, auto
from typing import ClassVar, Literal
from ..config import settings
@@ -7,6 +8,12 @@
from .user import User
+class FlagCheckResult(Enum):
+ invalid = auto()
+ valid = auto()
+ invalid_sign = auto()
+
+
class Flag(EBaseModel):
__public_fields__: ClassVar = {"classtype"}
__admin_only_fields__: ClassVar = {"flag_base"}
@@ -21,16 +28,16 @@ def sanitization(self, user_flag: str) -> str:
user_flag = user_flag[:-1]
user_flag = self.flag_base + "{" + user_flag + "}"
- return user_flag
+ return user_flag # noqa: RET504
def flag_value(self, user: User) -> str:
return self.flag_base + "{test_flag}"
- def flag_checker(self, user_flag: str, user: User) -> bool:
+ def flag_checker(self, user_flag: str, user: User) -> FlagCheckResult:
if self.flag_value(user) == self.sanitization(user_flag):
- return True
- else:
- return False
+ return FlagCheckResult.valid
+
+ return FlagCheckResult.invalid
class StaticFlag(Flag):
@@ -49,7 +56,24 @@ class DynamicKKSFlag(Flag):
classtype: Literal["DynamicKKSFlag"] = "DynamicKKSFlag"
dynamic_flag_base: str
- def flag_value(self, user: User) -> str:
+ def flag_parts(self, user: User) -> tuple[str, str]:
flag_part = "{" + self.dynamic_flag_base + "}" + f"{user.user_id}"
hash = hmac.digest(settings.FLAG_SIGN_KEY.encode(), flag_part.encode(), "sha256")
- return self.flag_base + "{" + self.dynamic_flag_base + "_" + binascii.hexlify(hash).decode()[0:14] + "}"
+ return self.dynamic_flag_base, binascii.hexlify(hash).decode()[0:14]
+
+ def flag_value(self, user: User) -> str:
+ base, hash = self.flag_parts(user)
+ return self.flag_base + "{" + base + "_" + hash + "}"
+
+ def flag_checker(self, user_flag: str, user: User) -> FlagCheckResult:
+ sanitized_flag = self.sanitization(user_flag)
+ if self.flag_value(user) == sanitized_flag:
+ return FlagCheckResult.valid
+
+ # some of copypaste, but i have no idea how to make this without copypaste
+ base, hash = self.flag_parts(user)
+ prefix = self.flag_base + "{" + base
+ if sanitized_flag.startswith(prefix):
+ return FlagCheckResult.invalid_sign
+
+ return FlagCheckResult.invalid
diff --git a/app/schema/task.py b/app/schema/task.py
index 34d3b5b..c341747 100644
--- a/app/schema/task.py
+++ b/app/schema/task.py
@@ -1,9 +1,11 @@
import datetime
import uuid
+from enum import Enum
from typing import Annotated, ClassVar
from zoneinfo import ZoneInfo
-from pydantic import Field
+from pydantic import Field, computed_field
+from typing_extensions import TypeVar
from .. import config
from ..config import settings
@@ -38,6 +40,18 @@ def template_format_time(date: datetime.datetime) -> str: # from alb1or1x_shit.
]
+class DynamicTaskType(Enum):
+ BUILDER = "builder"
+ SERVICE = "service"
+
+
+class DynamicTaskInfo(EBaseModel):
+ __admin_only_fields__: ClassVar = {
+ "dynamic_task_type",
+ }
+ dynamic_task_type: DynamicTaskType
+
+
class Task(EBaseModel):
__public_fields__: ClassVar = {
"task_id",
@@ -52,6 +66,9 @@ class Task(EBaseModel):
"description",
"flag",
"hidden",
+ "points",
+ "solves",
+ "dynamic_task_info",
}
task_id: uuid.UUID = Field(default_factory=uuid.uuid4)
@@ -72,6 +89,8 @@ class Task(EBaseModel):
author: str
+ dynamic_task_info: DynamicTaskInfo | None = None
+
# @computed_field
@property
def color_category(self) -> str:
@@ -147,6 +166,19 @@ def first_pwned_str(self) -> tuple[uuid.UUID, str]:
def short_desc(self) -> str:
return f"task_id={self.task_id} task_name={self.task_name} hidden={self.hidden} points={self.scoring.points}"
+ @computed_field
+ @property
+ def points(self) -> int:
+ return self.scoring.points
+
+ @computed_field
+ @property
+ def solves(self) -> int:
+ return len(self.pwned_by)
+
+
+_T = TypeVar("_T", bound=Task)
+
class TaskForm(EBaseModel):
task_name: str
@@ -155,3 +187,22 @@ class TaskForm(EBaseModel):
description: str
flag: FlagUnion
author: str = ""
+
+ dynamic_task_info: DynamicTaskInfo | None = None
+
+ def to_task(self, cls: type[_T], author: User) -> _T:
+ str_author = self.author if self.author != "" else f"@{author.username}"
+ if not str_author.startswith("@"):
+ str_author = f"@{str_author}"
+
+ task = cls(
+ task_name=self.task_name,
+ category=self.category,
+ scoring=self.scoring,
+ description=self.description,
+ description_html=Task.regenerate_md(self.description),
+ flag=self.flag,
+ author=str_author,
+ dynamic_task_info=self.dynamic_task_info,
+ )
+ return task
diff --git a/app/test/__init__.py b/app/test/__init__.py
index 60d026d..b73fdb3 100644
--- a/app/test/__init__.py
+++ b/app/test/__init__.py
@@ -1,6 +1,7 @@
# ruff: noqa: S101, S106, ANN201, T201 # this is a __test file__
import typing
+from contextlib import contextmanager
import pytest
from fastapi.testclient import TestClient
@@ -127,5 +128,14 @@ def client(request):
client.__exit__()
+@contextmanager
+def enable_debug() -> typing.Generator[None, typing.Any, None]:
+ settings.DEBUG = True
+ try:
+ yield
+ finally:
+ settings.DEBUG = False
+
+
# from . import test_auth # noqa
# from . import test_main # noqa
diff --git a/app/test/test_auth.py b/app/test/test_auth.py
index 28126f5..3da0589 100644
--- a/app/test/test_auth.py
+++ b/app/test/test_auth.py
@@ -3,7 +3,7 @@
from fastapi import status
from .. import config, schema
-from . import ClientEx, app
+from . import ClientEx, app, enable_debug
from . import client as client_cl
client = client_cl
@@ -12,7 +12,7 @@
def test_register(client: ClientEx):
- resp = client.simple_register_raw(username="Rubikoid", password="123")
+ resp = client.simple_register_raw(username="Rubikoid", password="123456789")
assert resp.status_code == status.HTTP_200_OK, resp.text
assert resp.text == '"ok"', resp.text
@@ -20,16 +20,17 @@ def test_register(client: ClientEx):
def test_login(client: ClientEx):
test_register(client)
- resp = client.simple_login_raw(username="Rubikoid", password="123")
+ resp = client.simple_login_raw(username="Rubikoid", password="123456789")
assert resp.status_code == status.HTTP_200_OK, resp.text
assert resp.text == '"ok"', resp.text
def test_admin(client: ClientEx):
- # config.settings.DEBUG = True
- test_login(client)
- # config.settings.DEBUG = False
+ # need to enable debug here, because `Rubikoid-as-default-admin` is debug feature
+ with enable_debug():
+ test_login(client)
+
resp = client.get(app.url_path_for("api_admin_users_me"))
# print(resp.json())
assert resp.status_code == status.HTTP_200_OK, resp.text
@@ -37,40 +38,47 @@ def test_admin(client: ClientEx):
assert resp.json()["username"] == "Rubikoid", resp.json()
+def test_not_admin_without_debug(client: ClientEx):
+ test_login(client)
+ resp = client.get(app.url_path_for("api_admin_users_me"))
+ assert resp.status_code == status.HTTP_403_FORBIDDEN, resp.text
+
+
def test_admin_fail(client: ClientEx):
- resp1 = client.simple_register_raw(username="Not_Rubikoid", password="123")
- assert resp1.status_code == status.HTTP_200_OK, resp1.text
- assert resp1.text == '"ok"', resp1.text
+ with enable_debug():
+ resp1 = client.simple_register_raw(username="Not_Rubikoid", password="123456789")
+ assert resp1.status_code == status.HTTP_200_OK, resp1.text
+ assert resp1.text == '"ok"', resp1.text
- resp2 = client.simple_login_raw(username="Not_Rubikoid", password="123")
- assert resp2.status_code == status.HTTP_200_OK, resp2.text
- assert resp2.text == '"ok"', resp2.text
+ resp2 = client.simple_login_raw(username="Not_Rubikoid", password="123456789")
+ assert resp2.status_code == status.HTTP_200_OK, resp2.text
+ assert resp2.text == '"ok"', resp2.text
- resp3 = client.get(app.url_path_for("api_admin_users_me"))
- assert resp3.status_code == status.HTTP_403_FORBIDDEN, resp3.text
+ resp3 = client.get(app.url_path_for("api_admin_users_me"))
+ assert resp3.status_code == status.HTTP_403_FORBIDDEN, resp3.text
def test_not_existing_user(client: ClientEx):
resp1 = client.post(
app.url_path_for("api_auth_simple_login"),
- json=LoginForm(username="Not_Existing_Account", password="123").model_dump(mode="json"),
+ json=LoginForm(username="Not_Existing_Account", password="123456789").model_dump(mode="json"),
)
assert resp1.status_code == status.HTTP_401_UNAUTHORIZED, resp1.text
def test_invalid_password(client: ClientEx):
- resp1 = client.simple_register_raw(username="Not_Rubikoid", password="123")
+ resp1 = client.simple_register_raw(username="Not_Rubikoid", password="123456789")
assert resp1.status_code == status.HTTP_200_OK, resp1.text
assert resp1.text == '"ok"', resp1.text
- resp2 = client.simple_login_raw(username="Not_Rubikoid", password="1234")
+ resp2 = client.simple_login_raw(username="Not_Rubikoid", password="1234567890")
assert resp2.status_code == status.HTTP_401_UNAUTHORIZED, resp2.text
def test_register_existing_user(client: ClientEx):
- resp1 = client.simple_register_raw(username="Not_Rubikoid", password="123")
+ resp1 = client.simple_register_raw(username="Not_Rubikoid", password="123456789")
assert resp1.status_code == status.HTTP_200_OK, resp1.text
assert resp1.text == '"ok"', resp1.text
- resp2 = client.simple_register_raw(username="Not_Rubikoid", password="1234")
+ resp2 = client.simple_register_raw(username="Not_Rubikoid", password="1234567890")
assert resp2.status_code == status.HTTP_403_FORBIDDEN, resp2.text
diff --git a/app/test/test_main.py b/app/test/test_main.py
index d5c280f..f295f18 100644
--- a/app/test/test_main.py
+++ b/app/test/test_main.py
@@ -1,15 +1,9 @@
-import uuid
-
-import pytest
-
-from .. import schema
-from . import TestClient, app
+from . import TestClient
from . import client as client_cl
-from . import test_auth
client = client_cl
-def test_read_main(client: TestClient):
+def test_read_main(client: TestClient) -> None:
resp = client.get("/")
assert resp.status_code == 200
diff --git a/app/test/test_tasks_api.py b/app/test/test_tasks_api.py
index da4f8f1..6267b7e 100644
--- a/app/test/test_tasks_api.py
+++ b/app/test/test_tasks_api.py
@@ -66,7 +66,7 @@ def test_task_create(client: ClientEx):
def test_task_solve(client: ClientEx):
- client.simple_register_raw(username="Rubikoid", password="123")
+ test_auth.test_admin(client)
tasks: dict[int, schema.Task] = {} # fake array ;)
tasks[0] = client.create_task(
@@ -97,7 +97,7 @@ def test_task_solve(client: ClientEx):
task = tasks[i]
assert not task.hidden, f"{task = }"
- client.simple_register_raw(username="Rubikoid_user", password="123")
+ client.simple_register_raw(username="Rubikoid_user", password="123456789")
resp1 = client.solve_task_raw("test_task")
assert resp1.status_code == status.HTTP_200_OK, resp1.text
diff --git a/app/utils/tg.py b/app/utils/tg.py
index a5e4764..eb42778 100644
--- a/app/utils/tg.py
+++ b/app/utils/tg.py
@@ -1,6 +1,6 @@
from typing import cast
-import requests
+import httpx
from .. import schema
from ..config import settings
@@ -9,11 +9,12 @@
logger = get_logger("api")
-def to_tg(data: dict, path: str) -> requests.Response:
+def to_tg(data: dict, path: str) -> httpx.Response | None:
if not settings.BOT_TOKEN:
- return
+ return None
+
url = f"https://api.telegram.org/bot{settings.BOT_TOKEN}/{path}"
- ret = requests.post(url, data=data)
+ ret = httpx.post(url, data=data)
logger.info(f"TG info={ret.text}")
return ret
diff --git a/app/view/__init__.py b/app/view/__init__.py
index 3486f4f..1eaae9f 100644
--- a/app/view/__init__.py
+++ b/app/view/__init__.py
@@ -155,10 +155,9 @@ async def login_get(req: Request, resp: Response, user: auth.CURR_USER_SAFE):
async def tasks_get_task(
req: Request,
resp: Response,
- task_id: uuid.UUID,
+ task: api_tasks.CURRENT_TASK,
user: auth.CURR_USER_SAFE,
):
- task = await api_tasks.api_task_get(task_id, user)
return await response_generator(
req,
"task.jhtml",
diff --git a/app/view/admin/__init__.py b/app/view/admin/__init__.py
index 912e317..fa63f9f 100644
--- a/app/view/admin/__init__.py
+++ b/app/view/admin/__init__.py
@@ -14,16 +14,21 @@
WebSocketException,
status,
)
+from fastapi.responses import HTMLResponse
from fastapi.routing import APIRouter
+from fastui import AnyComponent, FastUI, prebuilt_html
+from fastui import components as c
+from fastui.components.display import DisplayLookup, DisplayMode
+from fastui.events import BackEvent, GoToEvent
from ... import auth, config, schema
-from ...api import api_tasks as api_tasks
-from ...api import api_users as api_users
-from ...api.admin import admin_checker
+from ...api import api_tasks, api_users
+from ...api.admin import CURR_ADMIN, admin_checker
from ...api.admin import admin_tasks as api_admin_tasks
from ...api.admin import admin_users as api_admin_users
from ...utils.log_helper import get_logger
from ...ws import ws_manager
+from .ng import api_rotuer, base_router
logger = get_logger("view")
@@ -33,10 +38,11 @@
prefix="/admin",
tags=["admin_view"],
)
+router.include_router(base_router)
@router.get("/")
-async def admin_index(req: Request, resp: Response, user: schema.User = Depends(admin_checker)):
+async def admin_index(req: Request, resp: Response, user: CURR_ADMIN):
return await response_generator(
req,
"admin/index.jhtml",
@@ -49,7 +55,7 @@ async def admin_index(req: Request, resp: Response, user: schema.User = Depends(
@router.get("/tasks")
-async def admin_tasks(req: Request, resp: Response, user: schema.User = Depends(admin_checker)):
+async def admin_tasks(req: Request, resp: Response, user: CURR_ADMIN):
tasks_list = await api_tasks.api_tasks_get(user)
return await response_generator(
req,
@@ -66,7 +72,7 @@ async def admin_tasks(req: Request, resp: Response, user: schema.User = Depends(
@router.get("/task/{task_id}")
-async def admin_task_get(req: Request, resp: Response, task_id: uuid.UUID, user: schema.User = Depends(admin_checker)):
+async def admin_task_get(req: Request, resp: Response, task_id: uuid.UUID, user: CURR_ADMIN):
tasks_list = await api_tasks.api_tasks_get(user)
selected_task = await api_tasks.api_task_get(task_id, user)
return await response_generator(
@@ -85,7 +91,7 @@ async def admin_task_get(req: Request, resp: Response, task_id: uuid.UUID, user:
@router.get("/users")
-async def admin_users(req: Request, resp: Response, user: schema.User = Depends(admin_checker)):
+async def admin_users(req: Request, resp: Response, user: CURR_ADMIN):
users_dict = await api_admin_users.api_admin_users_internal()
return await response_generator(
req,
@@ -102,19 +108,18 @@ async def admin_users(req: Request, resp: Response, user: schema.User = Depends(
@router.get("/user/{user_id}")
-async def admin_user_get(req: Request, resp: Response, user_id: uuid.UUID, user: schema.User = Depends(admin_checker)):
+async def admin_user_get(req: Request, resp: Response, admin: CURR_ADMIN, user: api_admin_users.CURR_USER):
users_dict = await api_admin_users.api_admin_users_internal()
- selected_user = await api_admin_users.api_admin_user_get_internal(user_id)
return await response_generator(
req,
"admin/users_admin.jhtml",
{
"request": req,
- "curr_user": user,
+ "curr_user": admin,
"user_class": schema.User,
# "user_form_class": schema.UserForm,
"users_list": users_dict.values(),
- "selected_user": selected_user,
+ "selected_user": user,
},
ignore_admin=False,
)
@@ -143,3 +148,16 @@ async def websocker_ep(
await websocket.receive_text()
except WebSocketDisconnect:
ws_manager.disconnect(websocket)
+
+ # return await response_generator(
+ # req,
+ # "admin/tasks_admin.jhtml",
+ # {
+ # "request": req,
+ # "curr_user": user,
+ # "task_class": schema.Task,
+ # "task_form_class": schema.TaskForm,
+ # "tasks_list": tasks_list,
+ # },
+ # ignore_admin=False,
+ # )
diff --git a/app/view/admin/ng.py b/app/view/admin/ng.py
new file mode 100644
index 0000000..8175fb2
--- /dev/null
+++ b/app/view/admin/ng.py
@@ -0,0 +1,172 @@
+import uuid
+
+from fastapi import (
+ Cookie,
+ Depends,
+ FastAPI,
+ HTTPException,
+ Query,
+ Request,
+ Response,
+ WebSocket,
+ WebSocketDisconnect,
+ WebSocketException,
+ status,
+)
+from fastapi.responses import HTMLResponse
+from fastapi.routing import APIRouter
+from fastui import AnyComponent, FastUI, prebuilt_html
+from fastui import components as c
+from fastui.components.display import DisplayLookup, DisplayMode
+from fastui.events import BackEvent, GoToEvent
+
+from ... import auth, config, schema
+from ...api import api_tasks, api_users
+from ...api.admin import CURR_ADMIN, admin_checker
+from ...api.admin import admin_tasks as api_admin_tasks
+from ...api.admin import admin_users as api_admin_users
+from ...utils.log_helper import get_logger
+from ...ws import ws_manager
+
+logger = get_logger("view")
+
+base_router = APIRouter(
+ prefix="/ng",
+ tags=["admin-ng"],
+)
+
+api_rotuer = APIRouter(
+ prefix="/api/admin/ng",
+ tags=["admin-ng-api"],
+)
+
+
+def url_gen(req: Request, path: str) -> str:
+ return str(req.url_for("admin_ng_html_landing", path=path))
+
+
+def base_page(req: Request, *components: AnyComponent, title: str | None = None) -> list[AnyComponent]:
+ return [
+ c.PageTitle(text=f"YATB Admin — {title if title else 'Root'}"),
+ c.Navbar(
+ title="YATB Admin",
+ title_event=GoToEvent(url=url_gen(req, "")),
+ start_links=[
+ c.Link(
+ components=[c.Text(text="Tasks")],
+ on_click=GoToEvent(url=url_gen(req, "tasks")),
+ active="startswith:/tasks",
+ ),
+ c.Link(
+ components=[c.Text(text="Users")],
+ on_click=GoToEvent(url=url_gen(req, "users")),
+ active="startswith:/users",
+ ),
+ # c.Link(
+ # components=[c.Text(text="Components")],
+ # on_click=GoToEvent(url="/components"),
+ # active="startswith:/components",
+ # ),
+ ],
+ ),
+ c.Page(
+ components=[
+ *((c.Heading(text=title),) if title else ()),
+ *components,
+ ],
+ ),
+ ]
+
+
+@api_rotuer.get("", response_model=FastUI, response_model_exclude_none=True)
+async def admin_ng_index(req: Request, admin: CURR_ADMIN) -> list[AnyComponent]:
+ return base_page(req, c.Text(text="..."))
+
+
+@api_rotuer.get("/tasks", response_model=FastUI, response_model_exclude_none=True)
+async def admin_ng_tasks(req: Request, admin: CURR_ADMIN) -> list[AnyComponent]:
+ tasks = await api_admin_tasks.api_admin_tasks(admin)
+
+ return base_page(
+ req,
+ # c.ModelForm(submit_url="", model=schema.TaskForm),
+ c.Table(
+ data=[
+ schema.Task.admin_model.model_validate(v.model_dump())
+ for i, v in sorted(tasks.items(), key=lambda iv: iv[1].task_name)
+ ],
+ data_model=schema.Task.admin_model,
+ columns=[
+ DisplayLookup(field="task_name", title="Name", on_click=GoToEvent(url=url_gen(req, "task/{task_id}"))),
+ DisplayLookup(field="category", title="Category"),
+ DisplayLookup(field="points", title="Points"),
+ DisplayLookup(field="solves", title="Solve Count"),
+ ],
+ ),
+ title="Tasks",
+ )
+
+
+@api_rotuer.get("/task/{task_id}", response_model=FastUI, response_model_exclude_none=True)
+async def admin_ng_task(req: Request, admin: CURR_ADMIN, raw_task: api_admin_tasks.CURR_TASK) -> list[AnyComponent]:
+ sanitized_task = schema.Task.admin_model.model_validate(raw_task.model_dump())
+
+ return base_page(
+ req,
+ c.Heading(text=sanitized_task.task_name, level=2),
+ c.Link(components=[c.Text(text="Back")], on_click=BackEvent()),
+ c.Details(
+ data=sanitized_task,
+ # fields=[
+ # DisplayLookup(field="task_id", title="ID"),
+ # ],
+ ),
+ title=f"Task - {sanitized_task.task_name}",
+ )
+
+
+@api_rotuer.get("/users", response_model=FastUI, response_model_exclude_none=True)
+async def admin_ng_users(req: Request, admin: CURR_ADMIN) -> list[AnyComponent]:
+ users = await api_admin_users.api_admin_users(admin)
+
+ return base_page(
+ req,
+ # c.ModelForm(submit_url="", model=schema.TaskForm),
+ c.Table(
+ data=[
+ schema.User.admin_model.model_validate(v.model_dump())
+ for i, v in sorted(users.items(), key=lambda iv: iv[1].username)
+ ],
+ data_model=schema.User.admin_model,
+ columns=[
+ DisplayLookup(field="username", title="Name", on_click=GoToEvent(url=url_gen(req, "user/{user_id}"))),
+ DisplayLookup(field="is_admin", title="Admin"),
+ # DisplayLookup(field="points", title="Points"),
+ # DisplayLookup(field="solves", title="Solve Count"),
+ ],
+ ),
+ title="Users",
+ )
+
+
+@api_rotuer.get("/user/{user_id}", response_model=FastUI, response_model_exclude_none=True)
+async def admin_ng_user(req: Request, admin: CURR_ADMIN, raw_user: api_admin_users.CURR_USER) -> list[AnyComponent]:
+ sanitized_user = schema.User.admin_model.model_validate(raw_user.model_dump())
+
+ return base_page(
+ req,
+ c.Heading(text=sanitized_user.username, level=2),
+ c.Link(components=[c.Text(text="Back")], on_click=BackEvent()),
+ c.Details(
+ data=sanitized_user,
+ # fields=[
+ # DisplayLookup(field="task_id", title="ID"),
+ # ],
+ ),
+ title=f"User - {sanitized_user.username}",
+ )
+
+
+@base_router.get("/{path:path}")
+async def admin_ng_html_landing() -> HTMLResponse:
+ return HTMLResponse(prebuilt_html(title="YATB admin.."))
diff --git a/app/view/static/dynamic_tasks.js b/app/view/static/dynamic_tasks.js
new file mode 100644
index 0000000..1e158ea
--- /dev/null
+++ b/app/view/static/dynamic_tasks.js
@@ -0,0 +1,67 @@
+function load_all_dynamic_info() {
+ tasks = $(".dynamic_task_info_field").map((idx, info_field) => {
+ return info_field.dataset.id;
+ }).get();
+
+ req(api_list["api_dynamic_task_infos"], { data: { "tasks": tasks } })
+ .then(get_json)
+ .then((data) => { return data; }, nok_toast_generator("Get dynamic task info"))
+ .then((data) => {
+ Object.entries(data.json.data).forEach(([task_id, info]) => {
+ set_status(task_id, info);
+ });
+ });
+};
+
+function set_status(task_id, info) {
+ $("#data-" + task_id).html(info);
+ updateMacy();
+}
+
+$(".dynamic_task_info").click(function (event) {
+ event.preventDefault();
+ let task_id = this.dataset.id;
+ preq(api_list["api_dynamic_task_info"], { "task_id": task_id }, { method: 'GET' })
+ .then(get_text)
+ .then((data) => { return data; }, nok_toast_generator("Get dynamic task info"))
+ .then((data) => {
+ set_status(task_id, data.text);
+ });
+});
+
+$(".dynamic_task_start").click(function (event) {
+ event.preventDefault();
+ let task_id = this.dataset.id;
+
+
+ set_status(task_id, "Task building in process")
+
+ preq(api_list["api_dynamic_task_start"], { "task_id": task_id }, { method: 'GET' })
+ .then(get_text)
+ .then(ok_toast_generator("Start dynamic task"), nok_toast_generator("Start dynamic task"))
+ .then((data) => {
+ set_status(task_id, data.text);
+ });
+});
+
+$(".dynamic_task_stop").click(function (event) {
+ event.preventDefault();
+ let task_id = this.dataset.id;
+ preq(api_list["api_dynamic_task_stop"], { "task_id": task_id }, { method: 'GET' })
+ .then(get_text)
+ .then(ok_toast_generator("Stop dynamic task"), nok_toast_generator("Stop dynamic task"))
+ .then((data) => {
+ set_status(task_id, data.text);
+ });
+});
+
+$(".dynamic_task_restart").click(function (event) {
+ event.preventDefault();
+ let task_id = this.dataset.id;
+ preq(api_list["api_dynamic_task_restart"], { "task_id": task_id }, { method: 'GET' })
+ .then(get_text)
+ .then(ok_toast_generator("Restart dynamic task"), nok_toast_generator("Restart dynamic task"))
+ .then((data) => {
+ set_status(task_id, data.text);
+ });
+});
diff --git a/app/view/static/style.css b/app/view/static/style.css
index 9ba8ed4..a581b17 100644
--- a/app/view/static/style.css
+++ b/app/view/static/style.css
@@ -38,6 +38,16 @@
}
}
+@font-face {
+ font-family: "Spoof Medium";
+ src: url(https://of.worldota.net/fonts/spoof/Spoof-Medium.woff2);
+}
+
+@font-face {
+ font-family: "PT Root UI";
+ src: url(https://of.worldota.net/fonts/ptrootui/pt-root-ui_vf.woff2);
+}
+
html {
position: relative;
min-height: 100%;
@@ -48,11 +58,23 @@ body {
/* Margin top by header height */
margin-bottom: 60px;
/* Margin bottom by footer height */
- background-color: #585858;
+ /* background-color: #acef81; */
+ font-family: "PT Root UI";
+ background-color: #f2f1f0;
+}
+
+.marine-text {
+ color: rgb(13, 65, 210);
+ font-size: 16px;
+ font-weight: 500;
}
body>nav {
- background-color: #404040;
+ background-color: white;
+}
+
+.navbar-brand {
+ font-family: "Spoof Medium";
}
.footer {
@@ -63,30 +85,19 @@ body>nav {
height: 60px;
line-height: 60px;
/* Vertically center the text there */
- color: #909090;
- background-color: #404040;
}
-div.card>* a {
- text-decoration: none;
+.card {
+ background-color: #fff;
}
-div.card.solved:not(.secondary) {
- background: repeating-linear-gradient(135deg,
- #404040,
- #404040 20px,
- #585858 20px,
- #585858 40px);
- color: white;
-}
-
-div.card.solved.secondary {
- background: #404040;
- color: white;
+div.card>* a {
+ text-decoration: none;
}
-div.card.solved>div.card-header>*>* {
- color: greenyellow;
+div.card.solved {
+ background: #acef81;
+ color: #000;
}
div.card.solved>.card-body>.card-text>a {
diff --git a/app/view/templates/admin/base.jhtml b/app/view/templates/admin/base.jhtml
index bb4103d..643864a 100644
--- a/app/view/templates/admin/base.jhtml
+++ b/app/view/templates/admin/base.jhtml
@@ -7,6 +7,7 @@
{% set head_data.page_name = "YATB ADMIN" %}
{% set head_data.pages = {
+ ("NG ADMIN",): url_for('admin_ng_html_landing', path=""),
("TASKS ADMIN",): url_for('admin_tasks'),
("USERS ADMIN",): url_for('admin_users'),
("Back to board",): url_for('index'),
diff --git a/app/view/templates/base.jhtml b/app/view/templates/base.jhtml
index e7eeb6d..f7197cb 100644
--- a/app/view/templates/base.jhtml
+++ b/app/view/templates/base.jhtml
@@ -16,7 +16,7 @@
-
+
@@ -33,53 +33,60 @@
{% set header_data.yatb_logo_text = CTF_NAME %}
{% block header %}
-