sacn_accout_system/server/main.py

356 lines
12 KiB
Python
Raw Normal View History

2024-08-17 11:25:51 +08:00
from fastapi.security import OAuth2PasswordBearer
from fastapi import FastAPI, Cookie, Response, Form
from fastapi.templating import Jinja2Templates
2024-08-20 21:37:47 +08:00
from fastapi.responses import RedirectResponse, HTMLResponse
2024-08-17 11:25:51 +08:00
from datetime import timedelta, datetime
from contextlib import asynccontextmanager
from . import db
from . import email as email_sys
2024-08-25 20:51:14 +08:00
from . import cfg
2024-08-30 17:43:49 +08:00
from . import admin
2024-08-17 11:25:51 +08:00
import hashlib
import uuid
from typing import Annotated
import re
import asyncio
import uvicorn
2024-08-25 20:51:14 +08:00
from . import cfg
2024-08-25 20:05:24 +08:00
import pygtrie
2024-08-25 20:51:14 +08:00
import os
import sys
import importlib
2024-08-17 11:25:51 +08:00
2024-08-25 20:51:14 +08:00
def load_cfg():
global ACCESS_TOKEN_EXPIRE_MINUTES
global ACCESS_EMAIL_EXPIRE_MINUTES
global ROOT
global CLEAN_TIMEOUT
global MANAGE_KEY
global REDIRECT_URL_WHITELIST
ACCESS_TOKEN_EXPIRE_MINUTES = cfg.config["common"]["access_token_expire_minutes"]
ACCESS_EMAIL_EXPIRE_MINUTES = cfg.config["common"]["access_email_expire_minutes"]
ROOT = cfg.config["common"]["root"]
CLEAN_TIMEOUT = cfg.config["common"]["clean_timeout"]
MANAGE_KEY = cfg.config["common"]["manage_key"]
REDIRECT_URL_WHITELIST = cfg.config["common"]["redirect_url_whitelist"]
load_cfg()
2024-08-17 11:25:51 +08:00
2024-08-25 20:05:24 +08:00
tokens = pygtrie.StringTrie()
apikeys = pygtrie.StringTrie()
emails = pygtrie.StringTrie()
2024-08-17 11:25:51 +08:00
email_send_lst = []
2024-08-25 20:05:24 +08:00
def prep_uuid(uuid: str):
return '/'.join(list(uuid))
def clean_uuid(uuid: str):
return uuid.replace("/", "")
2024-08-30 17:43:49 +08:00
async def run_clean():
sys.stderr.write("==> clean\n")
for k, v in tokens.items():
if (v[1] < datetime.now()):
del tokens[k]
for k, v in apikeys.items():
if (v[1] < datetime.now()):
del apikeys[k]
for k, v in emails.items():
if (v[2] < datetime.now()):
del emails[k]
2024-08-17 11:25:51 +08:00
async def clean_sys():
while 1:
await asyncio.sleep(CLEAN_TIMEOUT)
2024-08-30 17:43:49 +08:00
await run_clean()
2024-08-17 11:25:51 +08:00
async def send_email():
while 1:
if (len(email_send_lst) > 0):
infos = email_send_lst.pop(0)
asyncio.create_task(email_sys.sendemail(infos[0], infos[1]))
await asyncio.sleep(0.1)
else:
await asyncio.sleep(5)
@asynccontextmanager
async def lifespan(app: FastAPI):
await db.connect_db()
asyncio.create_task(clean_sys())
asyncio.create_task(send_email())
yield
app = FastAPI(lifespan=lifespan)
templates = Jinja2Templates(directory="src")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def check_passwd(passwd: str):
if (len(passwd) < 8):
return 1
2024-08-20 21:37:47 +08:00
pattern = r'^(?![a-zA-Z]+$)(?!\d+$)(?![^\da-zA-Z\s]+$).{8,40}$'
2024-08-17 11:25:51 +08:00
if re.match(pattern, passwd):
return 0
else:
return 1
2024-08-31 07:32:43 +08:00
def check_username(passwd: str):
if (len(passwd) < 3):
return 1
pattern = r'[A-Za-z0-9\_\-]{3,16}'
if re.match(pattern, passwd):
return 0
else:
return 1
2024-08-17 11:25:51 +08:00
async def authenticate_user(username: str, password: str):
hashed_password = await db.get_user(username)
if not hashed_password:
return False
return hashed_password == hashlib.sha256(
password.encode("utf-8")).hexdigest()
async def create_token(username: str):
2024-08-25 20:05:24 +08:00
tkn = prep_uuid(uuid.uuid4().hex)
2024-08-17 11:25:51 +08:00
tokens[tkn] = (username, datetime.now() +
timedelta(minutes=float(ACCESS_TOKEN_EXPIRE_MINUTES)))
return tkn
async def check_token(tkn: str):
res = tokens.get(tkn, None)
if (res is None):
return ""
if (res[1] < datetime.now()):
del tokens[tkn]
return ""
return res[0]
async def check_apikey(tkn: str):
res = apikeys.get(tkn, None)
if (res is None):
return ""
if (res[1] < datetime.now()):
del apikeys[tkn]
return ""
return res[0]
@app.post("/api/login")
async def login_callback(response: Response, username: str = Form(), password: str = Form()):
2024-08-31 07:32:43 +08:00
if (check_username(username)):
return {"msg": cfg.lang["invalid_username"]}
if (check_passwd(password)):
return {"msg": cfg.lang["weak_passwd"]}
2024-08-17 11:25:51 +08:00
if (await authenticate_user(username, password)):
tokennow = await create_token(username)
2024-08-25 20:05:24 +08:00
tkn = prep_uuid(uuid.uuid4().hex)
2024-08-17 11:25:51 +08:00
apikeys[tkn] = tokens[tokennow]
2024-08-25 20:05:24 +08:00
response.set_cookie("session", clean_uuid(tokennow))
2024-08-26 12:48:52 +08:00
return {"msg": "", "key": clean_uuid(tkn)}
2024-08-17 11:25:51 +08:00
else:
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["username_or_password_incorrect"], "key": ""}
2024-08-17 11:25:51 +08:00
regex = re.compile(
r'([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+')
@app.post("/api/signup")
async def login_callback(username: str = Form(), password: str = Form(), email: str = Form()):
if (check_passwd(password)):
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["weak_passwd"], "code": 1}
2024-08-31 07:32:43 +08:00
if (check_username(username)):
return {"msg": cfg.lang["invalid_username"]}
2024-08-17 11:25:51 +08:00
if (not re.fullmatch(regex, email)):
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["invalid_email"], "code": 1}
2024-08-17 11:25:51 +08:00
if not (await db.check_user(username)):
2024-08-25 20:05:24 +08:00
tkn = prep_uuid(uuid.uuid4().hex)
2024-08-17 11:25:51 +08:00
emails[tkn] = (username, hashlib.sha256(
password.encode("utf-8")).hexdigest(), datetime.now() +
timedelta(minutes=float(ACCESS_EMAIL_EXPIRE_MINUTES)), email)
2024-08-25 20:05:24 +08:00
email_send_lst.append(
(email, ROOT+"/api/checkemail?uid="+clean_uuid(tkn)))
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["verification_email"].format(ACCESS_EMAIL_EXPIRE_MINUTES), "code": 0}
2024-08-17 11:25:51 +08:00
else:
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["username_exists"], "code": 1}
2024-08-17 11:25:51 +08:00
@app.get("/api/checkemail")
2024-08-20 21:37:47 +08:00
async def checkemail(uid: str):
2024-08-25 20:05:24 +08:00
uid = prep_uuid(uid)
2024-08-17 11:25:51 +08:00
if (uid not in emails):
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["invalid_regid"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-17 11:25:51 +08:00
if (emails[uid][2] < datetime.now()):
del emails[uid]
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["invalid_regid"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-20 21:37:47 +08:00
if (emails[uid][1] == ""):
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["invalid_regid"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-17 11:25:51 +08:00
if await db.create_user(emails[uid][0], emails[uid][1], emails[uid][3]) == 0:
del emails[uid]
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["created_successfully"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-17 11:25:51 +08:00
else:
del emails[uid]
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["username_exists"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-17 11:25:51 +08:00
2024-08-20 21:37:47 +08:00
@app.get("/api/resetpasswd", response_class=HTMLResponse)
async def resetpasswd(uid: str, response: Response):
2024-08-25 20:05:24 +08:00
uid = prep_uuid(uid)
2024-08-20 21:37:47 +08:00
if (uid not in emails):
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["invalid_checkid"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-20 21:37:47 +08:00
if (emails[uid][2] < datetime.now()):
del emails[uid]
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["invalid_checkid"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-20 21:37:47 +08:00
if (emails[uid][1] != ""):
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["invalid_checkid"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-20 21:37:47 +08:00
tokennow = await create_token(emails[uid][0])
2024-08-25 20:05:24 +08:00
tkn = prep_uuid(uuid.uuid4().hex)
2024-08-20 21:37:47 +08:00
apikeys[tkn] = tokens[tokennow]
2024-08-25 20:05:24 +08:00
response.set_cookie("session", clean_uuid(tokennow))
2024-08-20 22:11:47 +08:00
del emails[uid]
2024-08-28 20:21:31 +08:00
return f'<html><head><meta http-equiv="refresh" content="0;url=/user"><title>{cfg.lang["redirect"]}</title></head><body>{cfg.lang["redirect"]}</body></html>'
2024-08-20 21:37:47 +08:00
@app.post("/api/send_resetpasswd")
async def resetpasswd(username: str = Form()):
2024-08-31 07:32:43 +08:00
if (check_username(username)):
return {"msg": cfg.lang["invalid_username"]}
2024-08-20 21:37:47 +08:00
if (await db.check_user(username)):
email = await db.get_email(username)
2024-08-25 20:05:24 +08:00
tkn = prep_uuid(uuid.uuid4().hex)
2024-08-20 21:37:47 +08:00
emails[tkn] = (username, "", datetime.now() +
timedelta(minutes=float(ACCESS_EMAIL_EXPIRE_MINUTES)), email)
2024-08-25 20:05:24 +08:00
email_send_lst.append(
(email, ROOT+"/api/resetpasswd?uid="+clean_uuid(tkn)))
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["verification_email"].format(ACCESS_EMAIL_EXPIRE_MINUTES), "code": 0}
2024-08-20 21:37:47 +08:00
else:
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["username_not_exists"], "code": 1}
2024-08-20 21:37:47 +08:00
2024-08-17 11:25:51 +08:00
@app.get("/api/getinfo")
async def get_user_info(uid: str):
2024-08-25 20:05:24 +08:00
uid = prep_uuid(uid)
2024-08-17 11:25:51 +08:00
username = await check_apikey(uid)
if (username == ""):
2024-08-28 20:21:31 +08:00
return {"code": 1, "msg": cfg.lang["invalid_token"], "data": {}}
2024-08-17 11:25:51 +08:00
return {"code": 0, "msg": "", "data": {"username": username, "email": await db.get_email(username)}}
@app.post("/api/changepasswd")
async def changepasswd(password: str = Form(), session: Annotated[str | None, Cookie()] = None):
if (check_passwd(password)):
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["weak_passwd"]}
2024-08-17 11:25:51 +08:00
if (session is not None):
2024-08-25 20:05:24 +08:00
session = prep_uuid(session)
2024-08-17 11:25:51 +08:00
username = await check_token(session)
if (username != ""):
await db.update_passwd(username, hashlib.sha256(
password.encode("utf-8")).hexdigest())
del tokens[session]
return {"msg": ""}
else:
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["invalid_token"]}
2024-08-17 11:25:51 +08:00
else:
2024-08-28 20:21:31 +08:00
return {"msg": cfg.lang["invalid_token"]}
2024-08-17 11:25:51 +08:00
@app.get("/login")
async def login(state: str = "", client_id: str = "", redirect_url: str = "/user", session: Annotated[str | None, Cookie()] = None):
now_redirect_url = redirect_url.replace(
"https://", "").replace("http://", "").split("#")[0].rstrip("/")
if (now_redirect_url not in REDIRECT_URL_WHITELIST):
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("checkemail.html", {"request": {}, "msg": cfg.lang["invalid_redirect_url"], "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-17 11:25:51 +08:00
if (session is not None):
2024-08-25 20:05:24 +08:00
session = prep_uuid(session)
2024-08-17 11:25:51 +08:00
username = await check_token(session)
if (username != ""):
2024-08-25 20:05:24 +08:00
tkn = prep_uuid(uuid.uuid4().hex)
2024-08-17 11:25:51 +08:00
apikeys[tkn] = tokens[session]
2024-08-26 12:48:52 +08:00
return RedirectResponse(url=redirect_url+f"#access_token={clean_uuid(tkn)}&token_type=Bearer&state={state}")
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("login.html", {"request": {}, "redirect_url": redirect_url, "state": state, "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-17 11:25:51 +08:00
@app.get("/signup")
async def login(redirect_url: str):
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("signup.html", {"request": {}, "redirect_url": redirect_url, "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-17 11:25:51 +08:00
@app.get("/user")
async def login(session: Annotated[str | None, Cookie()] = None):
if (session is not None):
2024-08-25 20:05:24 +08:00
session = prep_uuid(session)
2024-08-17 11:25:51 +08:00
username = await check_token(session)
if (username == ""):
return RedirectResponse(url="/login?redirect_url=/user")
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("manage.html", {"request": {}, "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-17 11:25:51 +08:00
2024-08-20 21:37:47 +08:00
@app.get("/resetpasswd")
async def resetpasswd(response: Response):
2024-08-28 20:21:31 +08:00
return templates.TemplateResponse("resetpasswd.html", {"request": {}, "ui": cfg.config["ui"], "lang": cfg.lang})
2024-08-20 21:37:47 +08:00
2024-08-17 11:25:51 +08:00
@app.get("/manager/init")
async def init(key: str):
if (key != MANAGE_KEY):
return 1
await db.create_db()
return 0
2024-08-25 20:51:14 +08:00
plugins = []
async def reload_cfg():
global plugins
cfg.reload()
load_cfg()
email_sys.load_cfg()
for i in plugins:
await i.reload()
@app.get("/manager/reload")
async def reload(key: str):
sys.stderr.write("==> reload config\n")
if (key != MANAGE_KEY):
return 1
await reload_cfg()
return 0
2024-08-30 17:43:49 +08:00
admin.bind_admin(app, templates, run_clean, tokens, reload_cfg)
2024-08-25 20:51:14 +08:00
for i in os.listdir("plugin"):
if (len(i.split(".")) != 2 or i.split(".")[1] != 'py'):
continue
if (i.split(".")[0] not in cfg.config):
sys.stderr.write("--> disable "+i.split(".")[0]+"\n")
continue
sys.stderr.write("==> load "+i.split(".")[0]+"\n")
plugins.append(importlib.import_module("plugin."+i.split(".")[0]))
plugins[-1].main(app, ROOT, apikeys)
2024-08-17 11:25:51 +08:00
def run():
uvicorn.run(app, host="0.0.0.0", port=8000)