完成后台

This commit is contained in:
cxykevin 2024-08-30 17:43:49 +08:00
parent e9c8d2710f
commit 10988194a8
4 changed files with 624 additions and 10 deletions

96
server/admin.py Normal file
View File

@ -0,0 +1,96 @@
from fastapi import FastAPI, Cookie, Response, Form, HTTPException
from fastapi.templating import Jinja2Templates
import hashlib
from . import cfg
from . import db
from typing import Annotated
def bind_admin(app: FastAPI, templates: Jinja2Templates, run_clean, tokens: list, reload_cfg):
@app.get("/admin/auth/{key}")
async def admin_auth(response: Response, key: str):
if (cfg.config["common"]["manage_key"] == key):
response.set_cookie("adminsession", key)
return {}
raise HTTPException(status_code=404)
@app.get("/admin")
async def admin(adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
return templates.TemplateResponse("admin.html", {"request": {}, "ui": cfg.config["ui"], "lang": cfg.lang})
@app.post("/admin/clean")
async def admin_clean(adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
await run_clean()
return {"msg": "Cleaned!"}
@app.post("/admin/init")
async def admin_init(adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
await db.create_db()
return {"msg": "Init!"}
@app.post("/admin/reload")
async def admin_reload(adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
await reload_cfg()
return {"msg": "Reloaded!"}
@app.get("/admin/getinfo")
async def admin_getinfo(adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
return {"users": await db.get_userscount(), "tokens": len(tokens), "msg": ""}
@app.get("/admin/cfg")
async def admin_getcfg(adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
with open("config/config.toml", 'r', encoding='utf-8') as file:
cfgs = file.read()
return {"cfg": cfgs, "msg": ""}
@app.post("/admin/cfg")
async def admin_setcfg(wcfg: str = Form(), adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
with open("config/config.toml", 'w', encoding='utf-8') as file:
file.write(wcfg)
return {"msg": "Saved!"}
@app.post("/admin/create")
async def admin_createuser(username: str = Form(), passwd: str = Form(), email: str = Form(), adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
if await db.create_user(username, hashlib.sha256(passwd.encode("utf-8")).hexdigest(), email) == 0:
return {"msg": "Created!"}
return {"msg": "Fail!"}
@app.get("/admin/users")
async def admin_getusers(page: int = 1, username: str = "", adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
PAGE_CNT = 20
users = await db.search_user(page, username, PAGE_CNT)
if (users is None):
users = []
return {"msg": "", "users": users, "pages": (((await db.search_user_len(page, username, PAGE_CNT))-1)//PAGE_CNT)+1}
@app.delete("/admin/users")
async def admin_deleteusers(username: str, adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
await db.delete_user(username)
return {"msg": ""}
@app.post("/admin/users")
async def admin_ch_passwd(username: str = Form(), passwd: str = Form(), adminsession: Annotated[str | None, Cookie()] = None):
if (cfg.config["common"]["manage_key"] != adminsession):
raise HTTPException(status_code=404)
await db.update_passwd(username, hashlib.sha256(passwd.encode("utf-8")).hexdigest())
return {"msg": ""}

View File

@ -27,6 +27,20 @@ async def connect_db():
) )
async def recreate():
global db
db.close()
db = await aiomysql.create_pool(
host=HOST,
port=PORT,
user=USER,
password=PASSWD,
db=DB,
maxsize=MAXSIZE,
minsize=MINSIZE
)
async def create_db(): async def create_db():
async with db.acquire() as conn: async with db.acquire() as conn:
await (await conn.cursor()).execute( await (await conn.cursor()).execute(
@ -64,6 +78,8 @@ async def create_user(username: str, password: str, email: str):
# 创建新用户 # 创建新用户
await cur.execute("INSERT INTO users (username, password, email, accoutpwd) VALUES (%s, %s, %s, %s)", (username, password, email, uuid.uuid4().hex)) await cur.execute("INSERT INTO users (username, password, email, accoutpwd) VALUES (%s, %s, %s, %s)", (username, password, email, uuid.uuid4().hex))
await conn.commit() await conn.commit()
await recreate()
return 0 return 0
@ -82,6 +98,16 @@ async def update_passwd(username: str, password: str):
async with conn.cursor() as cur: async with conn.cursor() as cur:
await cur.execute("UPDATE users SET password = %s WHERE username = %s", (password, username)) await cur.execute("UPDATE users SET password = %s WHERE username = %s", (password, username))
await conn.commit() await conn.commit()
await recreate()
return 0
async def delete_user(username: str):
async with db.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("DELETE FROM users WHERE username = %s;COMMIT;", (username))
await conn.commit()
await recreate()
return 0 return 0
@ -93,3 +119,40 @@ async def get_email(username: str):
if result: if result:
return result[0] return result[0]
return None return None
async def get_userscount():
async with db.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT COUNT(*) FROM users")
result = await cur.fetchone()
if result:
return result[0]
return None
async def search_user(page: int, username: str, PAGE_CNT):
async with db.acquire() as conn:
async with conn.cursor() as cur:
username_search = ('WHERE username LIKE "%%'+username +
'%%"' if username != "" else "")
cmd = 'SELECT * FROM users '+username_search+' LIMIT ' + \
str(page*PAGE_CNT-PAGE_CNT)+','+str(PAGE_CNT)+';'
await cur.execute(cmd)
result = await cur.fetchall()
if result:
return [(i[0], i[2]) for i in result]
return None
async def search_user_len(page: int, username: str, PAGE_CNT):
async with db.acquire() as conn:
async with conn.cursor() as cur:
username_search = ('WHERE username LIKE "%%'+username +
'%%"' if username != "" else "")
cmd = 'SELECT COUNT(*) FROM users '+username_search+';'
await cur.execute(cmd)
result = await cur.fetchone()
if result:
return result[0]
return None

View File

@ -7,6 +7,7 @@ from contextlib import asynccontextmanager
from . import db from . import db
from . import email as email_sys from . import email as email_sys
from . import cfg from . import cfg
from . import admin
import hashlib import hashlib
import uuid import uuid
from typing import Annotated from typing import Annotated
@ -52,9 +53,7 @@ def clean_uuid(uuid: str):
return uuid.replace("/", "") return uuid.replace("/", "")
async def clean_sys(): async def run_clean():
while 1:
await asyncio.sleep(CLEAN_TIMEOUT)
sys.stderr.write("==> clean\n") sys.stderr.write("==> clean\n")
for k, v in tokens.items(): for k, v in tokens.items():
if (v[1] < datetime.now()): if (v[1] < datetime.now()):
@ -67,6 +66,12 @@ async def clean_sys():
del emails[k] del emails[k]
async def clean_sys():
while 1:
await asyncio.sleep(CLEAN_TIMEOUT)
await run_clean()
async def send_email(): async def send_email():
while 1: while 1:
if (len(email_send_lst) > 0): if (len(email_send_lst) > 0):
@ -314,6 +319,8 @@ async def reload(key: str):
await reload_cfg() await reload_cfg()
return 0 return 0
admin.bind_admin(app, templates, run_clean, tokens, reload_cfg)
for i in os.listdir("plugin"): for i in os.listdir("plugin"):
if (len(i.split(".")) != 2 or i.split(".")[1] != 'py'): if (len(i.split(".")) != 2 or i.split(".")[1] != 'py'):
continue continue

448
src/admin.html Normal file
View File

@ -0,0 +1,448 @@
<html class="mdui-theme-auto">
<head>
<link rel="stylesheet" href="https://learn.study-area.org.cn/theme/css/mdui.css">
<script src="https://learn.study-area.org.cn/theme/js/mdui.global.js" type="text/javascript"></script>
<link href="https://learn.study-area.org.cn/theme/css/icons.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/ace/1.36.0/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/ace/1.36.0/theme-clouds_midnight.js" type="text/javascript"
charset="utf-8"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/ace/1.36.0/mode-toml.js" type="text/javascript"
charset="utf-8"></script>
<title>{{ui.title}}</title>
<style>
* {
font-family: Arial, Helvetica, sans-serif;
}
.pages {
display: block;
position: absolute;
width: calc(100% - 5rem);
height: calc(100% - 4rem);
max-height: calc(100% - 4rem);
top: 4rem;
left: 5rem;
right: 0;
bottom: 0;
}
.container {
display: block;
position: relative;
width: calc(100% - 40px);
height: calc(100% - 40px);
max-height: calc(100% - 40px);
min-width: calc(100% - 40px);
min-height: calc(100% - 40px);
margin-left: 20px;
margin-top: 20px;
margin-bottom: 20px;
}
.hide {
display: none;
}
.infocard {
width: 200px;
height: 120px;
margin-right: 8px;
margin-top: 4px;
}
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 300;
src: local("SourceCodePro") url('https://learn.study-area.org.cn/font/sourcecodepro.woff2') format('woff2');
}
.ace_editor,
.ace_text-input,
.ace_hidpi,
.ace_text-layer,
.ace_gutter-layer,
.ace_content,
.ace_gutter {
font-family: 'Source Code Pro' !important;
}
* {
font-family: 'Source Code Pro', Arial, Helvetica, sans-serif;
}
.btns {
margin: -10px;
}
</style>
</head>
<body style="margin:0;padding:0;">
<mdui-dialog id="dialog" close-on-overlay-click>
</mdui-dialog>
<mdui-snackbar placement="top" action="确定" closeable id="snackbar" auto-close-delay="0"></mdui-snackbar>
<mdui-dialog id="chpwd_inputdialog" headline="更改密码" description="长度需>8请至少包含大小写字母和数字" close-on-overlay-click>
<mdui-text-field id="inputdialog_text"></mdui-text-field>
<mdui-button slot="action" variant="text"
onclick="document.getElementById('chpwd_inputdialog').open = false">取消</mdui-button>
<mdui-button slot="action" variant="tonal" onclick="allowchangepasswd()">确认</mdui-button>
</mdui-dialog>
<mdui-dialog id="newuser_inputdialog" headline="创建用户" description="" close-on-overlay-click>
<mdui-text-field id="username" label="用户名"></mdui-text-field>
<mdui-text-field id="email" label="邮箱"></mdui-text-field>
<mdui-text-field id="passwd" label="密码"></mdui-text-field>
<mdui-button slot="action" variant="text"
onclick="document.getElementById('newuser_inputdialog').open = false">取消</mdui-button>
<mdui-button slot="action" variant="tonal" onclick="allowcreateuser()">确认</mdui-button>
</mdui-dialog>
<mdui-layout style="height: 100%">
<mdui-top-app-bar>
<mdui-top-app-bar-title>{{ui.prod_name}}</mdui-top-app-bar-title>
</mdui-top-app-bar>
<mdui-navigation-rail value="page_home">
<mdui-navigation-rail-item icon="home" value="page_home"
onclick="update_btn('page_home')">主页</mdui-navigation-rail-item>
<mdui-navigation-rail-item icon="settings" value="page_settings"
onclick="update_btn('page_settings')">配置</mdui-navigation-rail-item>
<mdui-navigation-rail-item icon="account_circle" value="page_user"
onclick="update_btn('page_user')">账户</mdui-navigation-rail-item>
</mdui-navigation-rail>
<mdui-layout-main style="height: 100%">
<div class="pages" id="page_home">
<div class="container">
<div style="display:block;height:128px;position:static">
<mdui-card class="infocard">
<div style="margin-left:20px">
<h2>用户数</h2>
<span id="users">unkonwn</span>
</div>
</mdui-card>
<mdui-card class="infocard">
<div style="margin-left:20px">
<h2>活跃用户数</h2>
<span id="tokens">unkonwn</span>
</div>
</mdui-card>
<!-- <div style="flex-grow: 1"></div> -->
<div style="position: absolute;top:40px;right:40px;display:block;">
<mdui-dropdown>
<mdui-button slot="trigger" end-icon="settings" stype="">管理功能</mdui-button>
<mdui-menu>
<mdui-menu-item onclick="init_db()">初始化数据库</mdui-menu-item>
<mdui-menu-item onclick="reload_cfg()">重载配置</mdui-menu-item>
<mdui-menu-item onclick="clean_cache()">清理过期token</mdui-menu-item>
</mdui-menu>
</mdui-dropdown>
</div>
</div>
</div>
</div>
<div class="pages hide" id="page_settings">
<div class="container">
<div id="editor" style="height: calc(100% - 72px); width: 100%">Loading...</div>
<div style="display:flex">
<mdui-button end-icon="save" onclick="save_cfg()">保存</mdui-button>
<span>注意:程序不会自动重载配置文件,错误的配置文件可能会导致程序崩溃!</span>
</div>
</div>
</div>
<div class="pages hide" id="page_user">
<div class="container">
<div class="mdui-table" style="display:block;height:100%;width:100%">
<table>
<thead>
<tr>
<th scope="col">用户名</th>
<th scope="col">邮箱</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody id="main_users">
</tbody>
<tfoot>
<tr>
<td scope="row" colspan="2">
<div style="display:flex">
<mdui-button onclick="user_search()">筛选</mdui-button>
<mdui-text-field label="" style="height:40px"
id="search_username"></mdui-text-field>
</div>
</td>
<td>
<span id="page_cnt">1/0</span>
<mdui-button onclick="user_prevpage()">上一页</mdui-button>
<mdui-button onclick="user_nextpage()">下一页</mdui-button><mdui-button-icon
onclick="newuser()" icon="control_point"></mdui-button-icon>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</mdui-layout-main>
</mdui-layout>
<script>
var now_userpage = 1;
var max_userpage = 0;
var search_str = "";
function user_refersh() {
const Http = new XMLHttpRequest();
const url = '/admin/users?page=' + now_userpage.toString() + "&username=" + search_str;
Http.open("GET", url);
Http.send();
Http.addEventListener('loadend', () => {
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
if (ret.msg == "") {
max_userpage = ret.pages
if (now_userpage > max_userpage) {
now_userpage = max_userpage
if (now_userpage == 0) {
now_userpage = 1
}
}
document.getElementById("page_cnt").innerText = now_userpage.toString() + "/" + max_userpage.toString()
var allstr = ""
ret.users.forEach((val) => {
var htmls = `
<tr>
<th scope="row">`+ val[0] + `</th>
<td>`+ val[1] + `</td>
<td>
<mdui-button variant="standard" icon="delete" onclick=delete_user("`+ val[0] + `") class="btns">删除用户</mdui-button>
<mdui-button variant="standard" icon="edit" onclick=ch_passwd_user("`+ val[0] + `") class="btns">更改密码</mdui-button>
</td>
</tr>`
allstr = allstr + htmls
})
document.getElementById("main_users").innerHTML = allstr
} else {
dialog(ret.msg)
}
})
}
function newuser() {
newuser_inputdialog.open = true
}
function allowcreateuser() {
const Http = new XMLHttpRequest();
const url = '/admin/create';
Http.open("POST", url);
var formData = new FormData();
formData.append('username', document.getElementById("username").value);
formData.append('email', document.getElementById("email").value);
formData.append('passwd', document.getElementById("passwd").value)
Http.send(formData);
Http.addEventListener('loadend', () => {
newuser_inputdialog.open = false
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
if (ret.msg != "") {
dialog(ret.msg)
}
})
}
var snackbar_callback = function () {
console.log(1)
}
function delete_user(username) {
snackbar.innerHTML = "删除用户 \"" + username + "\"?"
snackbar_callback = function () {
snackbar.actionLoading = true;
const Http = new XMLHttpRequest();
const url = '/admin/users?username=' + username;
Http.open("DELETE", url);
Http.send();
Http.addEventListener('loadend', () => {
snackbar.actionLoading = false;
snackbar.open = false
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
if (ret.msg != "") {
dialog(ret.msg)
}
user_refersh()
})
}
snackbar.open = true;
}
var allowchangepasswd = function () { }
function ch_passwd_user(username) {
document.getElementById("chpwd_inputdialog").open = true
allowchangepasswd = function () {
document.getElementById("chpwd_inputdialog").open = false
const Http = new XMLHttpRequest();
const url = '/admin/users';
Http.open("POST", url);
var formData = new FormData();
formData.append('username', username);
formData.append('passwd', document.getElementById("inputdialog_text").value);
Http.send(formData);
Http.addEventListener('loadend', () => {
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
if (ret.msg != "") {
dialog(ret.msg)
}
})
}
}
function user_search() {
now_userpage = 1
search_str = document.getElementById("search_username").value
user_refersh()
}
function user_prevpage() {
if (now_userpage > 1) {
now_userpage -= 1
user_refersh()
}
}
function user_nextpage() {
if (now_userpage < max_userpage) {
now_userpage += 1
user_refersh()
}
}
user_refersh()
var editor = null;
window.onload = function () {
snackbar.addEventListener("action-click", () => {
snackbar_callback()
})
const Http = new XMLHttpRequest();
const url = '/admin/cfg';
Http.open("GET", url);
Http.send();
Http.addEventListener('loadend', () => {
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
if (ret.msg == "") {
editor = ace.edit("editor")
editor.setTheme("ace/theme/clouds_midnight");
editor.setValue(ret.cfg)
setTimeout(() => {
editor.session.setMode("ace/mode/toml");
}, 100)
editor.setFontSize(14);
} else {
dialog(ret.msg)
}
})
}
var last_id = "page_home"
function dialog(str) {
document.getElementById("dialog").innerHTML = str
document.getElementById("dialog").setAttribute("open", "")
}
function update_btn(page) {
document.getElementById(last_id).classList.add("hide")
document.getElementById(page).classList.remove("hide")
last_id = page
}
function update_data() {
const Http = new XMLHttpRequest();
const url = '/admin/getinfo';
Http.open("GET", url);
Http.send();
Http.addEventListener('loadend', () => {
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
if (ret.msg == "") {
document.getElementById("users").innerText = ret.users
document.getElementById("tokens").innerText = ret.tokens
} else {
dialog(ret.msg)
}
})
}
function init_db() {
const Http = new XMLHttpRequest();
const url = '/admin/init';
Http.open("POST", url);
Http.send();
Http.addEventListener('loadend', () => {
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
dialog(ret.msg)
})
}
function reload_cfg() {
const Http = new XMLHttpRequest();
const url = '/admin/reload';
Http.open("POST", url);
Http.send();
Http.addEventListener('loadend', () => {
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
dialog(ret.msg)
})
}
function clean_cache() {
const Http = new XMLHttpRequest();
const url = '/admin/clean';
Http.open("POST", url);
Http.send();
Http.addEventListener('loadend', () => {
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
dialog(ret.msg)
})
}
update_data()
setInterval(update_data, 10000)
function save_cfg() {
const Http = new XMLHttpRequest();
const url = '/admin/cfg';
Http.open("POST", url);
var formData = new FormData();
formData.append('wcfg', editor.getValue());
Http.send(formData);
Http.addEventListener('loadend', () => {
var ret = JSON.parse(Http.responseText)
if (ret.msg == undefined) {
return
}
dialog(ret.msg)
})
}
</script>
</body>
</html>