-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/oj' into develop
- Loading branch information
Showing
17 changed files
with
335 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
SECRET="THISISASECRET" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from .contest import OJContest | ||
from .user import User |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .database import contestscol, db, usercol |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .base import require_ccf_file |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.