diff --git a/app.py b/app.py index a692fae..dcbb4e6 100644 --- a/app.py +++ b/app.py @@ -21,21 +21,21 @@ services_path='services' if not os.path.exists(services_path): logger.critical(f"{os.path.join(os.getcwd(),services_path)} 不存在") - raise + raise OSError(f"{os.path.join(os.getcwd(),services_path)} 不存在") services = os.listdir(services_path) for service in services: try: entrypoint = importlib.import_module(f"{services_path}.{service}").entrypoint - matedata = entrypoint(settings) + metadata = entrypoint(settings) logger.warn("#"*50+f""" 加载{service} -\t作者{matedata['Author']} -\t版本{matedata['Version']} -\t描述{matedata['Describe']} +\t作者{metadata['Author']} +\t版本{metadata['Version']} +\t描述{metadata['Describe']} """+"#"*50) - application.include_router(matedata['Router']) - matedata['Init']() + application.include_router(metadata['Router']) + metadata['Init']() except Exception as e: logger.warn(f"加载{service}出错") logger.warn(e) diff --git a/services/verify/__init__.py b/services/verify/__init__.py new file mode 100644 index 0000000..d1a15e2 --- /dev/null +++ b/services/verify/__init__.py @@ -0,0 +1,31 @@ +""" + +""" + +from fastapi import APIRouter +from dynaconf import Dynaconf + +from . import router + +router.init(APIRouter(prefix="/verify"), APIRouter(prefix="/verify")) + + +def entrypoint(settings: Dynaconf): + return { + "Author": "Bernie H.", + "Version": "1.0.0", + "Describe": "验证微服务,主要用于验证用户的邮箱等。", + "InterRouter": router.inter_router, + "ExposeRouter": router.public_router, + "Init": lambda: init(settings), + } + + +g_settings = None + + +def init(settings: Dynaconf): + print("Verify service is starting...") + global g_settings + g_settings = settings + diff --git a/services/verify/db.py b/services/verify/db.py new file mode 100644 index 0000000..ddc743a --- /dev/null +++ b/services/verify/db.py @@ -0,0 +1,24 @@ +from pony.orm import Database, Required, PrimaryKey + +""" Create Database """ +db = Database() + + +class OutsideVerifyCode(db.Entity): + code = PrimaryKey(str) + session = Required(str) + expired = Required(int) + callbackURI = Required(str) + + +class InsideVerifyCode(db.Entity): + code = PrimaryKey(str) + session = Required(str) + expired = Required(int) + + +db.bind(provider="sqlite", filename="verify.db") + +db.generate_mapping(create_tables=True) + +from pony.orm import db_session diff --git a/services/verify/router.py b/services/verify/router.py new file mode 100644 index 0000000..0a871ea --- /dev/null +++ b/services/verify/router.py @@ -0,0 +1,160 @@ +""" +[service] Verify Service + +* this: 本服务 +* s1: 调用本服务的服务 +* client: 用户的客户端 + +1. [s1] 请求内网api "/newVerify" 来进行一个新的验证 (需提供一个回调地址、一个 session ID) +2. [client] 通过路由请求内部api "/verifyOutsideCode" 进行验证 +3. [this] 将 [client] 重定向到 " ?s= &v= " (`SUCCESS` 为 `0/1`, 代表验证失败/成功, `INSIDE-CODE` 为内部验证码) +4. [s1] 请求内网api "/verifyInsideCode" 验证 `INSIDE-CODE` +5. [s1] 根据返回字段 `ok` 判断验证是否成功, `sess` 来恢复会话 + +""" + +import fastapi +from fastapi import Request, Response +from starlette.responses import RedirectResponse + +import verify +import send + + +inter_router = None +public_router = None + + +def init(ri: fastapi.APIRouter, rp: fastapi.APIRouter): + global inter_router + global public_router + inter_router = ri + public_router = rp + + inter_router.add_route("/newVerifyCode", newVerifyCode, methods=["GET"]) + inter_router.add_route("/verifyOutsideCode", verifyOutsideCode, methods=["GET"]) + inter_router.add_route("/verifyInsideCode", verifyInsideCode, methods=["GET"]) + + public_router.add_route("/verify", verifyOutsideCode, methods=["GET"]) + + +def _getVerifyUri(code: str): + """ + [Tool] 从验证码生成验证链接 + """ + return f"https://openteens.org/verify/verify?code={code}" + +def newVerify(): + """ + [内部] 进行一个新的验证 + + Request: + method: str (one of [email]) + target: str (e.g. "test@openteens.org") + session: str + callbackURI: str (建议使用带https的绝对路径) + + Response: + ..codeblock:: json + { + code: int + } + """ + request = Request() + + method = request.args.get("method") + target = request.args.get("target") + session = request.args.get("session") + callbackURI = request.args.get("callbackURI") + + code = verify.genOutsideCode(session, callbackURI) + + if method == "email": + send.sendEmail(target, _getVerifyUri(code)) + else: + return {"code": -1} + + return {"code": 0} + + +def newVerifyCode(): + """ + [内部] 进行一个新的验证(返回验证链接) + + Request: + session: str + callbackURI: str (建议使用带https的绝对路径) + + Response: + ..codeblock:: json + { + code: int, + verifyUri: str + } + """ + request = Request() + + session = request.args.get("session") + callbackURI = request.args.get("callbackURI") + + code = verify.genOutsideCode(session, callbackURI) + + return {"code": 0, "verifyUri": _getVerifyUri(code)} + + +def verifyOutsideCode(): + """ + [外部] 验证外部验证码 + + Request: + code: str (外部验证码) + + Response: + (重定向到回调地址) + """ + request = Request() + + code = request.args.get("code") + + result = verify.verifyOutsideCode(code) + if result: + sess = result["session"] + callbackURI = result["callbackURI"] + + insideCode = verify.genInsideCode(sess) + redirectURI = f"{callbackURI}?s=1&v={insideCode}" + else: + redirectURI = "ERROR: Invalid Verification Link" + + return RedirectResponse(redirectURI) + + +def verifyInsideCode(): + """ + [内部] 验证内部验证码 + + Request: + code: str (内部验证码) + + Response: + ..codeblock:: json + { + code: int, + ok: bool, + sess: str + } + """ + request = Request() + + code = request.args.get("code") + + result = verify.verifyInsideCode(code) + if result: + return {"code": 0, "ok": True, "sess": result["session"]} + else: + return {"code": -1} + + +if __name__ == "__main__": + init(fastapi.APIRouter()) + # router.run("localhost", 5003) diff --git a/services/verify/send.py b/services/verify/send.py new file mode 100644 index 0000000..bcdba7c --- /dev/null +++ b/services/verify/send.py @@ -0,0 +1,161 @@ +def sendEmail(target, code): + """ + 发送验证邮件 + """ + + template = """ + + + + + 邮箱验证码 + + + + + + + + + +
+
+ + +
+
+
+
+ 尊敬的用户:您好! + + 您正在进行邮箱验证,请点击以下链接完成验证: + + 若不是您在操作,请忽略此邮件。 +
+ + 如果您无法点击以上链接,请将此链接复制到浏览器地址栏中访问。 + https://api.openteens.org/userVerify?code={} + +
+
+
+
+
+

此为系统邮件,请勿回复
+

+

—— OpenTeens 社区

+
+
+
+ + """ + + title = "OpenTeens 邮箱验证" + from_ = "OpenTeens " + to = target + content = template.format(code, code, code) + + diff --git a/services/verify/utils.py b/services/verify/utils.py new file mode 100644 index 0000000..e75e6e0 --- /dev/null +++ b/services/verify/utils.py @@ -0,0 +1,6 @@ +import random +from hashlib import sha256 + + +def genRandHash(): + return sha256(str(random.getrandbits(256)).encode()).hexdigest() diff --git a/services/verify/verify.py b/services/verify/verify.py new file mode 100644 index 0000000..b8947a5 --- /dev/null +++ b/services/verify/verify.py @@ -0,0 +1,66 @@ +import time + +import db +import utils + + +def genOutsideCode(session, callbackURI): + """Generate a new outside code""" + with db.db_session: + code = utils.genRandHash() + expired = int(time.time()) + 60 * 10 # 10 minutes + + db.OutsideVerifyCode( + code=code, session=session, expired=expired, callbackURI=callbackURI + ) + + return code + + +def genInsideCode(session): + """Generate a new inside code""" + with db.db_session: + code = utils.genRandHash() + expired = int(time.time()) + 60 * 1 # 1 minutes + + db.InsideVerifyCode(code=code, session=session, expired=expired) + + return code + + +def verifyOutsideCode(code): + """Verify the outside code""" + with db.db_session: + result = db.OutsideVerifyCode.get(code=code) + + # [Check]: If code is valid + if result is None: + return None + + result = result.to_dict() + db.OutsideVerifyCode[code].delete() + + # [Check]: If code is expired + if result["expired"] < time.time(): + return None + + return result + + +def verifyInsideCode(code): + """Verify the inside code""" + with db.db_session: + result = db.InsideVerifyCode.get(code=code) + + # [Check]: If code is valid + if result is None: + return None + + result = result.to_dict() + db.InsideVerifyCode[code].delete() + + # [Check]: If code is expired + if result["expired"] < time.time(): + return None + + return result