Skip to content

Commit

Permalink
Merge branch 'feature/oj' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
XYCode-Kerman committed Apr 15, 2024
2 parents cafab29 + ede3426 commit d4f7d44
Show file tree
Hide file tree
Showing 17 changed files with 335 additions and 9 deletions.
3 changes: 1 addition & 2 deletions ccf_parser/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pathlib
from typing import List, Literal
from typing import List, Literal, Optional

from pydantic import BaseModel

Expand All @@ -11,7 +11,6 @@ class CCFHeader(BaseModel):
path: pathlib.Path
description: str
contest_type: Literal['OI', 'IOI']
enable_oj: bool


class Contest(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SECRET="THISISASECRET"
8 changes: 6 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
18 changes: 14 additions & 4 deletions manager/cli/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import shutil
import time
import zipfile
from pathlib import Path
from typing import *
Expand All @@ -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
Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions online_judge/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .base import app as oj_app
from .base import start_oj_background
107 changes: 107 additions & 0 deletions online_judge/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 .oj_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, key=os.environ['SECRET'], 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']
)


def require_role(roles: List[str] = ['default']) -> Callable[[User], User]:
def wrapper(user: User = Depends(get_user)) -> User:
if user.role not in roles:
raise HTTPException(status_code=403, detail="不符合权限要求")

return user

return wrapper


@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]
26 changes: 26 additions & 0 deletions online_judge/base.py
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions online_judge/contests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import json
import uuid
from datetime import datetime
from pathlib import Path
from typing import *

import fastapi
from fastapi import APIRouter, Body, Depends, HTTPException
from tinydb import Query

import ccf_parser

from .auth import require_role
from .oj_models import OJContest
from .utils import contestscol
from .utils.dependencies import require_ccf_file

router = APIRouter(prefix='/contests', tags=['比赛'])


@router.get('/', name='获取注册在 OJ 中的比赛', response_model=List[OJContest])
async def get_contests():
return contestscol.all()


@router.post('/', name='在 OJ 中注册一个已存在的比赛', response_model=OJContest, dependencies=[Depends(require_role('admin'))])
async def register_contest_to_oj(
start_time: Annotated[datetime, Body()],
end_time: Annotated[datetime, Body()],
ccf_file: Path = Depends(require_ccf_file)
):
oj_contest = OJContest(
contest_id=uuid.uuid4(),
ccf_file=ccf_file,
start_time=start_time,
end_time=end_time
)

doc_id = contestscol.insert(oj_contest.model_dump(mode='json'))

query = Query()
return contestscol.get(doc_id=doc_id)


@router.put('/{contest_id}', name='更新 OJ 中已注册的比赛', response_model=OJContest, dependencies=[Depends(require_role('admin'))])
async def update_contest_in_oj(contest_id: str, contest: OJContest):
query = Query()

doc_id = contestscol.upsert(
contest.model_dump(mode='json'),
query.contest_id == contest_id
)[0]

return contestscol.get(doc_id=doc_id)


@router.delete('/{contest_id}', name='删除 OJ 中已注册的比赛', dependencies=[Depends(require_role('admin'))], responses={
200: {
'description': '成功删除',
'content': {'application/json': {'example': {'status': True}}}
}
})
async def delete_contest_in_oj(contest_id: str):
query = Query()

contestscol.remove(query.contest_id == contest_id)

return {
'status': True
}
2 changes: 2 additions & 0 deletions online_judge/oj_models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .contest import OJContest
from .user import User
21 changes: 21 additions & 0 deletions online_judge/oj_models/contest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from datetime import datetime
from pathlib import Path
from uuid import UUID

from pydantic import BaseModel

from ccf_parser import CCF


class OJContest(BaseModel):
"""有别于 ccf_parser 中的 Contest,更加详细的比赛信息需要从 ccf_parser 中获取"""
contest_id: UUID
ccf_file: Path

start_time: datetime
end_time: datetime

@property
def read_ccf(self) -> CCF:
data = self.ccf_file.read_text('utf-8')
return CCF.model_validate_json(data)
7 changes: 7 additions & 0 deletions online_judge/oj_models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydantic import BaseModel


class User(BaseModel):
username: str
password: str
role: str
1 change: 1 addition & 0 deletions online_judge/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .database import contestscol, db, usercol
7 changes: 7 additions & 0 deletions online_judge/utils/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import tinydb
from tinydb import TinyDB

db = TinyDB('./assets/oj_db.json', indent=4,
ensure_ascii=False, sort_keys=True)
usercol = db.table('users')
contestscol = db.table('contests')
1 change: 1 addition & 0 deletions online_judge/utils/dependencies/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .base import require_ccf_file
25 changes: 25 additions & 0 deletions online_judge/utils/dependencies/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path

import pydantic
from fastapi import HTTPException

from ccf_parser import CCF


def require_ccf_file(ccf_file: Path) -> Path:
if ccf_file.name != "ccf.json":
raise HTTPException(status_code=400, detail="不是一个 CCF 文件")

if not ccf_file.exists():
raise HTTPException(status_code=404, detail="CCF 文件不存在")

if not ccf_file.is_file():
raise HTTPException(status_code=400, detail="不是一个有效的文件")

try:
ccf = CCF.model_validate_json(ccf_file.read_text('utf-8'))
except pydantic.ValidationError as e:
raise HTTPException(
status_code=400, detail=f"CCF 文件格式错误: {e.errors()}")

return ccf_file
Loading

0 comments on commit d4f7d44

Please sign in to comment.