From 43d0c23045b4c6e2ae74692697c21f6eb554215e Mon Sep 17 00:00:00 2001 From: Rubikoid Date: Wed, 10 Jul 2024 03:35:44 +0300 Subject: [PATCH 1/4] Update requirements --- pyproject.toml | 5 +++-- requirements-dev.txt | 1 + requirements.txt | 10 +++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d8ee947..37674b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,6 @@ documentation = "https://github.com/kksctf/yatb" # black config enabled. [tool.black] line-length = 120 -target_version = ['py311'] include = '\.pyi?$' exclude = ''' @@ -56,6 +55,7 @@ fix = false # python 3.10 target? target-version = "py311" +[tool.ruff.lint] task-tags = ["TODO", "FIXME", "WTF", "XXX"] # rules... @@ -75,6 +75,7 @@ ignore = [ "D202", # | pydocstyle | No blank lines allowed after function docstring (found 1) # don't like it "D203", # | pydocstyle | 1 blank line required before class docstring # don't like it "D205", # | pydocstyle | 1 blank line required between summary line and description # don't like it + "D212", "EM102", # | ruff? | Exception must not use an f-string literal, assign to variable first # i care, but not this proj "ERA001", # | ruff? | commented out code # i know. and what? "F401", # | pyflakes | %r imported but unused # pylance cover it @@ -86,5 +87,5 @@ ignore = [ [tool.pytest.ini_options] addopts = "--pyargs app --cov=app" -env = ["YATB_DEBUG=True", "ENABLE_METRICS=false"] +env = ["DEBUG=False", "ENABLE_METRICS=false", "TESTING=True"] filterwarnings = ["ignore::DeprecationWarning"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 660ad42..264c231 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ pytest==7.1.2 pytest-cov==2.10.1 pytest-env==0.6.2 httpx==0.22.0 +ruff==0.5.1 diff --git a/requirements.txt b/requirements.txt index 17bce7d..fb8924f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -pydantic-settings==2.0.3 -pydantic==2.1.1 +pydantic-settings==2.14.5 +pydantic==2.8.2 fastapi==0.101.1 aiofiles==0.8.0 @@ -14,7 +14,11 @@ prometheus-fastapi-instrumentator==5.6.0 markupsafe==2.0.1 websockets==10.4 +beanie==1.26.0 + +fastui==0.6.0 + # beanie>=1.23.6,<2 -git+https://github.com/Rubikoid/beanie.git@encoder-fix +# git+https://github.com/Rubikoid/beanie.git@encoder-fix git+https://github.com/kksctf/formgen.git@master From e4d88a694fbba3052dcb23067cb5e48c76fb126a Mon Sep 17 00:00:00 2001 From: Rubikoid Date: Wed, 10 Jul 2024 03:38:32 +0300 Subject: [PATCH 2/4] Update and fix tests. Disable `DEBUG` for tests (it's not good to run tests on __debug__ build), encapsulate things, needed by tests (like DB resetting shield) to another `TESTING` env variable. --- app/config.py | 4 +++- app/db/beanie.py | 9 +++++--- app/schema/auth/simple.py | 3 ++- app/test/__init__.py | 10 +++++++++ app/test/test_auth.py | 46 ++++++++++++++++++++++---------------- app/test/test_main.py | 10 ++------- app/test/test_tasks_api.py | 4 ++-- 7 files changed, 52 insertions(+), 34 deletions(-) diff --git a/app/config.py b/app/config.py index cb3d7f7..ff0ade9 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" @@ -65,7 +67,7 @@ class Settings(BaseSettings): @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/beanie.py b/app/db/beanie.py index 2b1785e..58056e1 100644 --- a/app/db/beanie.py +++ b/app/db/beanie.py @@ -311,15 +311,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) 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/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 From 03832ea8b2587fc9fb822cb8a1654bd4bb65ea70 Mon Sep 17 00:00:00 2001 From: Rubikoid Date: Wed, 10 Jul 2024 04:56:26 +0300 Subject: [PATCH 3/4] Small refactoring in a few places --- app/api/admin/__init__.py | 8 ++++---- app/api/admin/admin_tasks.py | 3 ++- app/api/admin/admin_users.py | 36 ++++++++++++++++++------------------ app/api/api_users.py | 4 ++-- app/cli/cmd/__init__.py | 6 +++--- app/schema/ebasemodel.py | 2 +- app/schema/task.py | 14 +++++++++++++- 7 files changed, 43 insertions(+), 30 deletions(-) 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_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/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/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/task.py b/app/schema/task.py index 34d3b5b..a0036b0 100644 --- a/app/schema/task.py +++ b/app/schema/task.py @@ -3,7 +3,7 @@ from typing import Annotated, ClassVar from zoneinfo import ZoneInfo -from pydantic import Field +from pydantic import Field, computed_field from .. import config from ..config import settings @@ -52,6 +52,8 @@ class Task(EBaseModel): "description", "flag", "hidden", + "points", + "solves", } task_id: uuid.UUID = Field(default_factory=uuid.uuid4) @@ -147,6 +149,16 @@ 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) + class TaskForm(EBaseModel): task_name: str From d3a606f5fc44becffce7d35eab6df28d729e2177 Mon Sep 17 00:00:00 2001 From: Rubikoid Date: Wed, 10 Jul 2024 04:57:31 +0300 Subject: [PATCH 4/4] Add FastUI PoC --- app/__init__.py | 1 + app/view/admin/__init__.py | 40 +++++-- app/view/admin/ng.py | 172 ++++++++++++++++++++++++++++ app/view/templates/admin/base.jhtml | 1 + 4 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 app/view/admin/ng.py diff --git a/app/__init__.py b/app/__init__.py index 49bd99a..b2d56e5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -31,6 +31,7 @@ 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 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/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'),