first commit

This commit is contained in:
2026-08-13 21:24:16 +08:00
commit a306eee7d5
24 changed files with 2457 additions and 0 deletions
+320
View File
@@ -0,0 +1,320 @@
# -*- coding: utf-8 -*-
"""
ResLib License 申请系统后端 (FastAPI)
存储:data/requests/<id>.txt(DokuWiki 式纯文本)
公开:申请 / 状态查询 / 下载 license
管理:密码登录 -> 审批 -> 生成 res_center.lic
"""
import os
import re
import secrets
import time
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, Form, HTTPException, Request, Response
from fastapi.responses import FileResponse, PlainTextResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
from license import generate_lic, normalize_mac
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
REQ_DIR = DATA_DIR / "requests"
LIC_DIR = DATA_DIR / "licenses"
PKG_DIR = DATA_DIR / "packages"
STATIC_DIR = BASE_DIR / "static"
def load_admin_password() -> str:
env = os.environ.get("RESLIB_ADMIN_PASSWORD", "").strip()
if env:
return env
env_file = BASE_DIR / ".env"
if env_file.is_file():
for line in env_file.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, _, v = line.partition("=")
if k.strip() == "RESLIB_ADMIN_PASSWORD":
return v.strip()
return "reslib-admin"
ADMIN_PASSWORD = load_admin_password()
SESSION_FILE = DATA_DIR / "session.txt"
STATUS_PENDING = "pending"
STATUS_APPROVED = "approved"
STATUS_DENIED = "denied"
for d in (REQ_DIR, LIC_DIR, PKG_DIR):
d.mkdir(parents=True, exist_ok=True)
app = FastAPI(title="ResLib License System", docs_url=None, redoc_url=None)
# ---------------------------------------------------------------------------
# 工具
# ---------------------------------------------------------------------------
def new_request_id() -> str:
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
return f"{stamp}-{secrets.token_hex(3).upper()}"
def request_path(req_id: str) -> Path:
if not re.fullmatch(r"\d{8}-\d{6}-[0-9A-F]{6}", req_id):
raise HTTPException(400, "无效的申请编号")
return REQ_DIR / f"{req_id}.txt"
def read_request(req_id: str) -> dict:
path = request_path(req_id)
if not path.is_file():
raise HTTPException(404, "申请不存在")
data = {}
for line in path.read_text(encoding="utf-8").splitlines():
if ":" in line:
k, _, v = line.partition(":")
data[k.strip()] = v.strip()
return data
def write_request(data: dict):
path = request_path(data["id"])
content = "\n".join(f"{k}: {data.get(k, '')}" for k in (
"id", "created", "status", "mac", "company", "contact",
"email", "creo", "seats", "notes", "reason",
))
path.write_text(content + "\n", encoding="utf-8")
def save_lic(req_id: str, mac: str) -> Path:
lic_path = LIC_DIR / f"{req_id}.lic"
lic_path.write_text(generate_lic(mac) + "\n", encoding="ascii")
return lic_path
def new_session_token() -> str:
token = secrets.token_hex(24)
SESSION_FILE.write_text(token, encoding="utf-8")
return token
def get_session_token() -> str:
if SESSION_FILE.is_file():
return SESSION_FILE.read_text(encoding="utf-8").strip()
return ""
def is_admin(request: Request) -> bool:
cookie = request.cookies.get("reslib_admin")
return bool(cookie) and secrets.compare_digest(cookie, get_session_token())
def require_admin(request: Request):
if not is_admin(request):
raise HTTPException(401, "未登录或会话已失效")
# ---------------------------------------------------------------------------
# 公开 API
# ---------------------------------------------------------------------------
class ApplyForm(BaseModel):
mac: str = Field(..., min_length=1)
company: str = Field(..., min_length=1)
contact: str = Field(..., min_length=1)
email: str = Field(..., min_length=1)
creo: str = ""
seats: str = "1"
notes: str = ""
@app.post("/api/apply")
def api_apply(form: ApplyForm):
try:
mac = normalize_mac(form.mac)
except ValueError as e:
raise HTTPException(400, str(e))
if "@" not in form.email or "." not in form.email.split("@")[-1]:
raise HTTPException(400, "邮箱格式不正确")
req_id = new_request_id()
data = {
"id": req_id,
"created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"status": STATUS_PENDING,
"mac": mac,
"company": form.company.strip(),
"contact": form.contact.strip(),
"email": form.email.strip(),
"creo": form.creo.strip(),
"seats": form.seats.strip(),
"notes": form.notes.strip(),
"reason": "",
}
write_request(data)
return {"id": req_id, "status": STATUS_PENDING}
@app.get("/api/status/{req_id}")
def api_status(req_id: str):
data = read_request(req_id)
return {
"id": data.get("id"),
"status": data.get("status"),
"created": data.get("created"),
"reason": data.get("reason", ""),
}
@app.get("/api/license/{req_id}")
def api_license(req_id: str):
data = read_request(req_id)
if data.get("status") != STATUS_APPROVED:
raise HTTPException(403, "申请尚未通过审批")
lic_path = LIC_DIR / f"{req_id}.lic"
if not lic_path.is_file():
save_lic(req_id, data["mac"])
lic_path = LIC_DIR / f"{req_id}.lic"
return FileResponse(
lic_path, filename="res_center.lic",
media_type="application/octet-stream",
)
# ---------------------------------------------------------------------------
# 资源包(仅审核通过的用户可获取)
# ---------------------------------------------------------------------------
@app.get("/api/package/{req_id}")
def api_package_list(req_id: str):
data = read_request(req_id)
if data.get("status") != STATUS_APPROVED:
raise HTTPException(403, "资源包仅对审核通过的用户提供,请先完成 License 审批")
files = sorted(p.name for p in PKG_DIR.glob("*") if p.is_file())
if not files:
return {"files": []}
return {"files": files}
@app.get("/api/package/download/{req_id}/{filename}")
def api_package_download(req_id: str, filename: str):
data = read_request(req_id)
if data.get("status") != STATUS_APPROVED:
raise HTTPException(403, "资源包仅对审核通过的用户提供")
name = os.path.basename(filename)
path = PKG_DIR / name
if not path.is_file():
raise HTTPException(404, "资源包不存在")
return FileResponse(path, filename=name, media_type="application/octet-stream")
# ---------------------------------------------------------------------------
# 管理 API
# ---------------------------------------------------------------------------
@app.post("/api/admin/login")
def admin_login(response: Response, password: str = Form(...)):
if not secrets.compare_digest(password, ADMIN_PASSWORD):
raise HTTPException(401, "密码错误")
token = new_session_token()
response.set_cookie("reslib_admin", token, httponly=True, samesite="lax", max_age=86400 * 7)
return {"ok": True}
@app.post("/api/admin/logout")
def admin_logout(request: Request, response: Response):
response.delete_cookie("reslib_admin")
return {"ok": True}
@app.get("/api/admin/list")
def admin_list(request: Request):
require_admin(request)
items = []
for path in sorted(REQ_DIR.glob("*.txt"), reverse=True):
items.append(read_request(path.stem))
return items
@app.post("/api/admin/approve/{req_id}")
def admin_approve(req_id: str, request: Request):
require_admin(request)
data = read_request(req_id)
if data.get("status") == STATUS_APPROVED:
return {"ok": True, "id": req_id}
save_lic(req_id, data["mac"])
data["status"] = STATUS_APPROVED
data["reason"] = ""
write_request(data)
return {"ok": True, "id": req_id}
@app.post("/api/admin/deny/{req_id}")
def admin_deny(req_id: str, request: Request, reason: str = Form("")):
require_admin(request)
data = read_request(req_id)
data["status"] = STATUS_DENIED
data["reason"] = reason.strip()[:200]
write_request(data)
return {"ok": True, "id": req_id}
# ---------------------------------------------------------------------------
# 页面与静态资源
# ---------------------------------------------------------------------------
@app.get("/")
def index():
return FileResponse(STATIC_DIR / "home.html")
@app.get("/about/")
def about_page():
return FileResponse(STATIC_DIR / "about.html")
@app.get("/reslib/")
def reslib_home():
return FileResponse(STATIC_DIR / "index.html")
@app.get("/reslib/apply")
def reslib_apply():
return FileResponse(STATIC_DIR / "apply.html")
@app.get("/reslib/status")
def reslib_status():
return FileResponse(STATIC_DIR / "status.html")
@app.get("/reslib/package")
def reslib_package():
return FileResponse(STATIC_DIR / "package.html")
@app.get("/admin")
def admin_page(request: Request):
if not is_admin(request):
return RedirectResponse("/admin/login", status_code=302)
return FileResponse(STATIC_DIR / "admin.html")
@app.get("/admin/login")
def admin_login_page():
return FileResponse(STATIC_DIR / "admin_login.html")
@app.get("/admin/logout")
def admin_logout_page(request: Request):
return RedirectResponse("/admin/login", status_code=302)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.mount("/blog", StaticFiles(directory=STATIC_DIR / "blog", html=True, check_dir=False), name="blog")