From 7e94ec6c4b1c1cb03170d4ad9dbbb284c8240f56 Mon Sep 17 00:00:00 2001 From: XYCode Kerman Date: Sun, 14 Apr 2024 23:30:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(oj):=20=E5=AE=9E=E7=8E=B0oj=E7=9A=84?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example.env | 1 + main.py | 8 ++- manager/cli/base.py | 18 +++++-- online_judge/__init__.py | 2 + online_judge/auth.py | 95 ++++++++++++++++++++++++++++++++++ online_judge/base.py | 26 ++++++++++ online_judge/contests.py | 31 +++++++++++ online_judge/models/user.py | 7 +++ online_judge/utils/__init__.py | 1 + online_judge/utils/database.py | 6 +++ poetry.lock | 43 ++++++++++++++- pyproject.toml | 2 + 12 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 example.env create mode 100644 online_judge/__init__.py create mode 100644 online_judge/auth.py create mode 100644 online_judge/base.py create mode 100644 online_judge/contests.py create mode 100644 online_judge/models/user.py create mode 100644 online_judge/utils/__init__.py create mode 100644 online_judge/utils/database.py diff --git a/example.env b/example.env new file mode 100644 index 0000000..7110fc5 --- /dev/null +++ b/example.env @@ -0,0 +1 @@ +SECRET="THISISASECRET" \ No newline at end of file diff --git a/main.py b/main.py index ddf396f..6df343a 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,12 @@ +from manager import cli_app, start_server_background +from ccf_parser import CCF import json import pathlib -from ccf_parser import CCF -from manager import cli_app, start_server_background +import dotenv + +dotenv.load_dotenv() + if '__main__' == __name__: cli_app() diff --git a/manager/cli/base.py b/manager/cli/base.py index df6f5a9..3825d91 100644 --- a/manager/cli/base.py +++ b/manager/cli/base.py @@ -1,4 +1,5 @@ import shutil +import time import zipfile from pathlib import Path from typing import * @@ -13,6 +14,7 @@ from rich.progress import track from rich.text import Text +from online_judge import start_oj_background from utils import manager_logger from ..base import _start_server, start_server_background @@ -90,8 +92,16 @@ def intro(): @app.command(name='server') -def start_server_command(): # pragma: no cover - download_ited() +def start_server_command(manager: bool = True, oj: bool = True): # pragma: no cover + if manager: + download_ited() - manager_logger.info('访问 http://localhost:2568/editor 以访问ItsWA Manager。') - _start_server() + manager_logger.info( + '访问 http://localhost:2568/editor 以访问ItsWA Manager。') + start_server_background() + + if oj: + start_oj_background() + + while True: + time.sleep(10**9) diff --git a/online_judge/__init__.py b/online_judge/__init__.py new file mode 100644 index 0000000..84a4426 --- /dev/null +++ b/online_judge/__init__.py @@ -0,0 +1,2 @@ +from .base import app as oj_app +from .base import start_oj_background diff --git a/online_judge/auth.py b/online_judge/auth.py new file mode 100644 index 0000000..f87d262 --- /dev/null +++ b/online_judge/auth.py @@ -0,0 +1,95 @@ +import datetime +import os +from typing import * + +import fastapi +import jwt +import pydantic +from fastapi import APIRouter, Body, Depends, HTTPException +from fastapi.security import APIKeyCookie +from tinydb import Query + +from .models.user import User +from .utils import usercol + +router = APIRouter(prefix='/auth', tags=['用户验证']) +apikey_schema = APIKeyCookie(name='itswa-oj-apikey') + + +def get_apikey_decoded(apikey: Optional[str] = Depends(apikey_schema)) -> Dict[Any, Any]: + if not apikey: + raise HTTPException(status_code=401, detail="请提供API Key") + + try: + decoded = jwt.decode(apikey, algorithms=['HS256']) + except jwt.DecodeError: + raise HTTPException(status_code=401, detail="API Key无效") + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="API Key已过期") + except Exception as e: + raise HTTPException(status_code=500, detail=f"未知错误: {e}") + + return decoded + + +def get_user(decoded: Dict[Any, Any] = Depends(get_apikey_decoded)) -> User: + try: + decoded: User = User(**decoded) + except pydantic.ValidationError: + raise HTTPException(status_code=401, detail="API Key无效") + + User_Query = Query() + result = usercol.search(User_Query.username == decoded.username)[0] + + return User(**result) + + +def get_token(user: User) -> str: + return jwt.encode( + { + **user.model_dump(mode='json'), + 'exp': datetime.datetime.now() + datetime.timedelta(days=7) + }, + os.environ['SECRET'] + ) + + +@router.post('/login', name='登录', responses={ + 200: { + "description": "登录成功", + "content": { + "application/json": { + "example": { + 'token': 'user_token' + } + } + } + } +}) +async def user_login(username: Annotated[str, Body()], password: Annotated[str, Body()]): + User_Query = Query() + results = usercol.search(User_Query.username == + username and User_Query.password == password) + + if results.__len__() >= 1: + return { + 'token': get_token(User.model_validate(results[0])) + } + else: + raise HTTPException(status_code=401, detail="用户名或密码错误") + + +@router.post('/register', name='注册', response_model=User) +async def user_register(username: Annotated[str, Body()], password: Annotated[str, Body()]): + User_Query = Query() + + if usercol.search(User_Query.username == username).__len__() >= 1: + raise HTTPException(status_code=409, detail="用户名已存在") + + usercol.insert(User( + username=username, + password=password, + role='default' + ).model_dump(mode='json')) + + return usercol.search(User_Query.username == username)[0] diff --git a/online_judge/base.py b/online_judge/base.py new file mode 100644 index 0000000..58f6197 --- /dev/null +++ b/online_judge/base.py @@ -0,0 +1,26 @@ +import multiprocessing + +import fastapi +import uvicorn + +from utils import online_judge_logger as logger + +from .auth import router as auth_router +from .contests import router as contests_router + +app = fastapi.FastAPI(title='ItsWA Online Judge API') +app.include_router(contests_router) +app.include_router(auth_router) + + +def _start_oj(): # proagma: no cover + logger.info('Online Judge API 启动, 地址 http://0.0.0.0:6572/') + uvicorn.run('online_judge:oj_app', host="0.0.0.0", port=6572, + workers=6, log_level='warning') + + +def start_oj_background(): # pragma: no cover + process = multiprocessing.Process(target=_start_oj) + process.start() + + return process diff --git a/online_judge/contests.py b/online_judge/contests.py new file mode 100644 index 0000000..e69974b --- /dev/null +++ b/online_judge/contests.py @@ -0,0 +1,31 @@ +import json +from pathlib import Path +from typing import * + +import fastapi +from fastapi import APIRouter, HTTPException + +import ccf_parser + +router = APIRouter(prefix='/contests', tags=['比赛']) + + +@router.get('/', response_model=List[ccf_parser.CCF]) +async def get_contests(): + contest_indexes_path = Path('./config/contests.json') + contest_indexes = [ + ccf_parser.ContestIndex.model_validate(x) + for x in json.loads(contest_indexes_path.read_text('utf-8')) + ] + + ccfs: List[ccf_parser.CCF] = [ + ccf_parser.CCF.model_validate_json( + x.ccf_file.joinpath('ccf.json').read_text('utf-8')) + for x in contest_indexes + ] + + # 抹除题目数据 + for ccf in ccfs: + ccf.contest.problems = [] + + return ccfs diff --git a/online_judge/models/user.py b/online_judge/models/user.py new file mode 100644 index 0000000..27c5771 --- /dev/null +++ b/online_judge/models/user.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class User(BaseModel): + username: str + password: str + role: str diff --git a/online_judge/utils/__init__.py b/online_judge/utils/__init__.py new file mode 100644 index 0000000..8a99a68 --- /dev/null +++ b/online_judge/utils/__init__.py @@ -0,0 +1 @@ +from .database import db, usercol diff --git a/online_judge/utils/database.py b/online_judge/utils/database.py new file mode 100644 index 0000000..2f4a0da --- /dev/null +++ b/online_judge/utils/database.py @@ -0,0 +1,6 @@ +import tinydb +from tinydb import TinyDB + +db = TinyDB('./assets/oj_db.json', indent=4, + ensure_ascii=False, sort_keys=True) +usercol = db.table('users') diff --git a/poetry.lock b/poetry.lock index 3e232f1..59a8a48 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1335,6 +1335,28 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "mirrors" +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pymdown-extensions" version = "10.7.1" @@ -1499,6 +1521,25 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "mirrors" +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pyyaml" version = "6.0.1" @@ -2070,4 +2111,4 @@ reference = "mirrors" [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "5f7250e8cac054f017d988fc870a6eb14576f554dc8255db9353526c06b66230" +content-hash = "f16126ccbbca1639e2117b92a9b33613105a0563df7fed581399840d8076ec80" diff --git a/pyproject.toml b/pyproject.toml index b017b21..8dc911a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ requests = "^2.31.0" pytest-html = "^4.1.1" dominate = "^2.9.1" tinydb = "^4.8.0" +pyjwt = "^2.8.0" +python-dotenv = "^1.0.1" [[tool.poetry.source]]