From 50f35fb6479bedad39b2452503f341f71779a95c Mon Sep 17 00:00:00 2001 From: p23 Date: Sun, 22 Dec 2024 18:03:55 +0000 Subject: [PATCH] route change --- app.py | 34 ++- blueprints/admin.py | 334 ---------------------------- blueprints/admin/article.py | 109 +++++++++ blueprints/admin/ig.py | 36 +++ blueprints/admin/setting.py | 32 +++ blueprints/admin/user.py | 140 ++++++++++++ blueprints/admin/utils.py | 67 ++++++ blueprints/{ => general}/article.py | 78 ++----- blueprints/general/comment.py | 15 ++ blueprints/{ => misc}/log.py | 7 +- blueprints/owner/owner_article.py | 58 +++++ requirements.txt | 3 +- settings.json | 2 +- utils/dbhelper.py | 2 + 14 files changed, 507 insertions(+), 410 deletions(-) delete mode 100644 blueprints/admin.py create mode 100644 blueprints/admin/article.py create mode 100644 blueprints/admin/ig.py create mode 100644 blueprints/admin/setting.py create mode 100644 blueprints/admin/user.py create mode 100644 blueprints/admin/utils.py rename blueprints/{ => general}/article.py (57%) create mode 100644 blueprints/general/comment.py rename blueprints/{ => misc}/log.py (89%) create mode 100644 blueprints/owner/owner_article.py diff --git a/app.py b/app.py index fcdb4b3..4645591 100644 --- a/app.py +++ b/app.py @@ -7,9 +7,22 @@ from sqlalchemy import create_engine from utils import setting_loader, logger, dbhelper from utils.pgclass import Base, SQLuser from utils.platform_consts import PLIST_ROOT -from blueprints.article import article -from blueprints.log import log -from blueprints.admin import admin + + +# import blueprints +# admin +from blueprints.admin.article import bl_admin_article +from blueprints.admin.ig import bl_admin_ig +from blueprints.admin.setting import bl_admin_setting +from blueprints.admin.user import bl_admin_user +# owner +from blueprints.owner.owner_article import bl_owner_article +# general +from blueprints.general.article import bl_article +from blueprints.general.comment import bl_comment +# misc +from blueprints.misc.log import bl_log + # env PG_HOST = os.getenv("PG_HOST", None).strip() @@ -56,9 +69,18 @@ app = Flask(__name__) app.config["SECRET_KEY"] = os.urandom(64) # register blueprints -app.register_blueprint(article, url_prefix = "/article") -app.register_blueprint(log , url_prefix = "/log") -app.register_blueprint(admin , url_prefix = "/admin") +# admin +app.register_blueprint(bl_admin_ig, url_prefix="/admin/ig") +app.register_blueprint(bl_admin_setting, url_prefix="/admin/setting") +app.register_blueprint(bl_admin_user, url_prefix="/admin/user") +app.register_blueprint(bl_admin_article, url_prefix="/admin") +# owner +app.register_blueprint(bl_owner_article, url_prefix="/owner") +# general +app.register_blueprint(bl_article, url_prefix="/article") +app.register_blueprint(bl_comment, url_prefix="/comment") +# misc +app.register_blueprint(bl_log, url_prefix = "/log") # logger logger.logger("server.start", "Server is running") diff --git a/blueprints/admin.py b/blueprints/admin.py deleted file mode 100644 index 3f5ea0f..0000000 --- a/blueprints/admin.py +++ /dev/null @@ -1,334 +0,0 @@ -import os -import time -import math -import json - -import jwt -from flask import Blueprint, request, jsonify, make_response, g, abort -from bcrypt import hashpw, gensalt, checkpw -from functools import wraps - -from utils import pgclass, setting_loader, logger, dbhelper, ighelper -from utils.misc import error, internal_json2protobuf -from utils.platform_consts import PLIST, PLIST_ROOT -from protobuf_files import niming_pb2 - -admin = Blueprint("admin", __name__) - -# jwt = {"id":user.id, "user":user.user, "exp":time.time} - -# auth decorator -def role_required(permreq: list): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - # get data 嘗試解碼jwt - key = os.getenv("JWT_KEY", None) - jwtsession = request.cookies.get("token", None) - if jwtsession == None: return error("You do not have permission to view this page."), 401 - jwtsession = str(jwtsession) - try: jwtdata = jwt.decode(jwt = jwtsession, key = key, algorithms = ["HS256"]) - except jwt.exceptions.ExpiredSignatureError: return error("Token expired!"), 401 - except jwt.exceptions.DecodeError: return error("Invalid token!"), 401 - if "id" not in jwtdata or "user" not in jwtdata: return error("Invalid token!"), 401 - - # db 驗證帳號是否正確 - table = pgclass.SQLuser - with dbhelper.db.getsession() as session: - res = session.query(table).filter(table.user == jwtdata["user"], table.id == jwtdata["id"]).first() - if res is None: return error("You do not have permission to view this page."), 401 - - # permission check 確保用戶有此路徑要求的權限 並且權限名稱皆合法 - permissionList = list(set(res.permission)) - for p in permissionList: # 檢查用戶JWT是否有不合法的權限名稱 - if p not in PLIST_ROOT: return error("The user has invalid permission."), 402 - for p in list(set(permreq)): - if p not in permissionList: return error("You do not have permission to view this page."), 402 - - # return - g.opuser = res - return f(*args, **kwargs) - return decorated_function - return decorator - -# login -@admin.route("/login", methods=["POST"]) -def login(): - # args - if "username" not in request.json or "password" not in request.json: return error("Arguments error"), 400 - username = str(request.json["username"]) - password = str(request.json["password"]) - - # variables - settings = setting_loader.loadset() - exptime = int(settings["JWT_Valid_Time"]) - - # db - table = pgclass.SQLuser - with dbhelper.db.getsession() as session: u = session.query(table).filter(table.user==username).first() - # auth - if u is None: return error("Login Failed"), 401 # 找不到用戶 - if not checkpw(password.encode("utf-8"), u.password.encode("utf-8")): return error("Login Failed"), 401 # 密碼沒法跟hash對上 - - # jwt - key = os.getenv("JWT_KEY", None) - if key is None: return error("JWT_KEY error"), 500 - jwtdata = {"id": u.id, "user":username, "exp":int(math.floor(time.time() + exptime))} - jwtdata = jwt.encode(payload = jwtdata, key = str(key), algorithm = "HS256") - - # logger - logger.logger("login", "User:%s logined"%username) - - # cookie - r = make_response("Access Granted") - r.set_cookie("token", jwtdata) - return r, 200 - -@admin.route("me", methods=["GET"]) -@role_required([]) -def user_me(): - opuser = g.opuser - return jsonify({"id":opuser.id, "user":opuser.user, "permission":opuser.permission}), 200 - -#################### -# User Area # -#################### -# list / get / add / delete -@admin.route("/user/list", methods={"GET"}) -@role_required([]) -def user_list(): - table = pgclass.SQLuser - with dbhelper.db.getsession() as session: users = session.query(table).all() - res = [ {"id":u.id, "user":u.user, "permission":u.permission} for u in users ] - return jsonify(res), 200 - -@admin.route("/user/", methods=["GET"]) -@role_required([]) -def user_get(id:int): - table = pgclass.SQLuser - with dbhelper.db.getsession() as session: u = session.query(table).filter(table.id==int(id)).first() - if u is None: return error("User not found"), 404 - return jsonify({"id":u.id, "user":u.user, "permission":u.permission}), 200 - -@admin.route("/user/", methods=["DELETE"]) -@role_required(["usermgr"]) -def user_del(id:int): - # db - table = pgclass.SQLuser - - with dbhelper.db.getsession() as session: - opuser = g.opuser # user who requested - - # check root - tguser = session.query(table).filter(table.id==int(id)).first() - if tguser is None: return error("User is not exist"), 400 - if tguser.user == "root": return error("You cannot delete user:root"), 400 - - # delete - session.delete(tguser) - session.commit() - - logger.logger("user.delete", "User:%s deleted an user:%s"%(opuser.user, tguser.user)) # logger - return jsonify({"result":"OK"}), 200 - -@admin.route("/user", methods=["POST"]) -@role_required(["usermgr"]) -def user_add(): - # db - table = pgclass.SQLuser - with dbhelper.db.getsession() as session: - # user who requested - opuser = g.opuser - - # payload - if "username" not in request.json or "password" not in request.json or \ - "permission" not in request.json or not(isinstance(request.json["permission"], list)): - return error("Arguments error"), 400 - username = str(request.json["username"]) - password = str(request.json["password"]) - permission = list(set([ str(p) for p in list(request.json["permission"]) ])) - # check username and password - if username == None or len(username) == 0 or password is None or len(password) == 0: - return error("Invalid Username or Password!"), 400 - # check permission list - for p in permission: - if p not in PLIST: return error("Invalid Permission"), 400 # 如果添加的權限名稱不合法 - if p not in opuser.permission: return error("You don't have the permission: %s"%p), 402 # 如果用戶本身不具有相同權限 - - # add - users = session.query(table).filter(table.user==username).first() - if users is None: # check whether the user already exist - pwhash = hashpw(password.encode("utf-8"), gensalt()).decode("utf-8") - session.add(table(user=username, password=pwhash, permission=permission)) - session.commit() - logger.logger("user.create", "User:%s created a new user:%s"%(opuser.user, username)) # logger - return jsonify({"user":username, "permission":permission}), 200 - else: - return error("User already exist!"), 400 - -#################### -# Article Area # -#################### -# list / get / pend / delete / fileget - -@admin.route("/article/file/", methods = ["GET"]) -@role_required(["article.read"]) -def article_fileget(fnhash:str): - resp, code = dbhelper.solo_file_fetcher("admin", fnhash) - return resp, code - - -@admin.route('/article/list', methods = ["GET"]) -@role_required(["article.read"]) -def article_list(): - res, code = dbhelper.multi_article_fetcher("admin", request.args.get("page"), 80) - return res, code - - -def check_key(type:str, key:str | int) -> str | int: - if type == 'a': - if not (len(key) > 0 and key.isdigit()): - return abort(400) - outkey = int(key) # id - elif type == 'c': - if not (len(key) > 0): - return abort(400) - outkey = str(key) # sha1 - else: - return abort(404) - - return outkey - - -# get article / comment -@admin.route("/article//", methods=["GET"]) -@role_required(["article.read"]) -def article_read(type:str, key:str): - key = check_key(type, key) - - if type == 'a': - res, code = dbhelper.solo_article_fetcher("admin", key) - elif type == 'c': - res, code = dbhelper.solo_comment_fetcher("admin", key) - - if code == 200: - return internal_json2protobuf(role="admin", otype=type, original=[res]), code - return abort(code) - - -# delete article / comment -@admin.route("/article//", methods=["DELETE"]) -@role_required(["article.del"]) -def article_del(type:str, key:str): - key = check_key(type, key) - - opuser = g.opuser - if type == 'a': - rtype = niming_pb2.AdminFetchPostResponse - result, code = dbhelper.solo_article_remover(role="admin", id=key, opuser=opuser.user) - elif type == 'c': - rtype = niming_pb2.AdminFetchCommentResponse - result, code = dbhelper.solo_comment_remover(role="admin", sha1=key, opuser=opuser.user) - - if not code == 200: # Exception - return abort(code) - - if type == 'a': - appobj = rtype.Message(id=result["id"], mark=result["mark"]) - elif type == 'c': - appobj = rtype.Message(sha1=result["sha1"], mark=result["mark"]) - ret = rtype() - ret.posts.append(appobj) - - return ret.SerializeToString(), 200 - - -# pend article / comment -@admin.route("/article//", methods=["PUT"]) -@role_required(["article.pend"]) -def article_pend(type:str, key:str): - key = check_key(type, key) - # 找到本體 - if type == 'a': - tg, code = dbhelper.solo_article_fetcher(role="admin", key=key) - elif type == 'c': - tg, code = dbhelper.solo_comment_fetcher(role="admin", key=key) - - if code != 200: - return abort(code) - - # db - mark = pgclass.SQLmark - with dbhelper.db.getsession() as session: - res = session.query(mark).filter(mark.hash==tg["hash"]).first() - if res is None: - return abort(404) - - # 如果文章已經公開 - if res.mark == "visible": - return abort(400) - elif res.mark == "pending": - res.mark = "visible" - session.commit() - - # run IG Post - if type == 'a': - result, err = ighelper.request_upload(tg["id"]) - if err or result["result"] == "Canceled delete post request": - return abort(500) - - return "OK", 200 - else: - return abort(500) - -#################### -# Setting Area # -#################### -# get / set -@admin.route("/setting", methods=["GET"]) -@role_required(["setting.edit"]) -def setting_get(): - return jsonify(setting_loader.loadset()), 200 - -@admin.route("/setting", methods=["POST"]) -@role_required(["setting.edit"]) -def setting_edit(): - opuser = g.opuser.user - - req = request.json - d = None - for r in req: - d = setting_loader.writeset(r, req.get(r)) - if d == 0: return error("Failed"), 401 - logger.logger("setting.modify", "User:%s modified settings: %s"%(opuser, json.dumps(request.json))) - - return jsonify(d), 200 - -#################### -# IGAPI Area # -#################### -@admin.route("/ig/accinfo", methods=["GET"]) -@role_required(["ig.accinfo"]) -def ig_accinfo(): - result, err = ighelper.request_account_info() - if err: - return jsonify(result), 500 - else: - return jsonify(result), 200 - - -@admin.route("/ig/login", methods=["GET"]) -@role_required(["ig.login"]) -def ig_login(): - result, err = ighelper.request_login() - if err: - return jsonify(result), 500 - else: - return jsonify(result), 200 - - -@admin.route("/ig/queue", methods=["GET"]) -@role_required(["ig.queue"]) -def ig_queue(): - result = ighelper.request_queue() - return jsonify(result), 200 diff --git a/blueprints/admin/article.py b/blueprints/admin/article.py new file mode 100644 index 0000000..5f4a074 --- /dev/null +++ b/blueprints/admin/article.py @@ -0,0 +1,109 @@ +from flask import Blueprint, request, g, abort + +from blueprints.admin.utils import check_key, role_required +from utils import pgclass, dbhelper, ighelper +from utils.misc import internal_json2protobuf +from protobuf_files import niming_pb2 + +# prefix: /admin +bl_admin_article = Blueprint("admin_article", __name__) + +# 這裡要改 +# get file +@bl_admin_article.route("/article/file/", methods = ["GET"]) +@role_required(["article.read"]) +def article_fileget(fnhash:str): + resp, code = dbhelper.solo_file_fetcher("admin", fnhash) + return resp, code + + +# list articles +@bl_admin_article.route('/article/list', methods = ["GET"]) +@role_required(["article.read"]) +def article_list(): + res, code = dbhelper.multi_article_fetcher("admin", request.args.get("page"), 80) + return res, code + + +# get article / comment +@bl_admin_article.route("//", methods=["GET"]) +@role_required(["article.read"]) +def article_read(type:str, key:str): + key = check_key(type, key) + + if type == 'article': + type = 'a' + res, code = dbhelper.solo_article_fetcher("admin", key) + elif type == 'comment': + type = 'c' + res, code = dbhelper.solo_comment_fetcher("admin", key) + + if code == 200: + return internal_json2protobuf(role="admin", otype=type, original=[res]), code + return abort(code) + + +# delete article / comment +@bl_admin_article.route("//", methods=["DELETE"]) +@role_required(["article.del"]) +def article_del(type:str, key:str): + key = check_key(type, key) + + opuser = g.opuser + if type == 'article': + rtype = niming_pb2.AdminFetchPostResponse + result, code = dbhelper.solo_article_remover(role="admin", id=key, opuser=opuser.user) + elif type == 'comment': + rtype = niming_pb2.AdminFetchCommentResponse + result, code = dbhelper.solo_comment_remover(role="admin", sha1=key, opuser=opuser.user) + + if not code == 200: # Exception + return abort(code) + + if type == 'article': + appobj = rtype.Message(id=result["id"], mark=result["mark"]) + elif type == 'comment': + appobj = rtype.Message(sha1=result["sha1"], mark=result["mark"]) + ret = rtype() + ret.posts.append(appobj) + + return ret.SerializeToString(), 200 + + +# pend article / comment +@bl_admin_article.route("//", methods=["PUT"]) +@role_required(["article.pend"]) +def article_pend(type:str, key:str): + key = check_key(type, key) + # 找到本體 + if type == 'article': + tg, code = dbhelper.solo_article_fetcher(role="admin", key=key) + elif type == 'comment': + tg, code = dbhelper.solo_comment_fetcher(role="admin", key=key) + + if code != 200: + return abort(code) + + # db + mark = pgclass.SQLmark + with dbhelper.db.getsession() as session: + res = session.query(mark).filter(mark.hash==tg["hash"]).first() + if res is None: + return abort(404) + + # 如果文章已經公開 + if res.mark == "visible": + return abort(400) + elif res.mark == "pending": + res.mark = "visible" + session.commit() + + # run IG Post + if type == 'article': + result, err = ighelper.request_upload(tg["id"]) + if err or result["result"] == "Canceled delete post request": + return abort(500) + + return "OK", 200 + else: + return abort(500) diff --git a/blueprints/admin/ig.py b/blueprints/admin/ig.py new file mode 100644 index 0000000..524b18c --- /dev/null +++ b/blueprints/admin/ig.py @@ -0,0 +1,36 @@ +from flask import Blueprint, jsonify + +from utils import ighelper +from blueprints.admin.utils import role_required + +# prefix: /admin/ig +bl_admin_ig = Blueprint("admin_ig", __name__) + +# get ig account info +@bl_admin_ig.route("/accinfo", methods=["GET"]) +@role_required(["ig.accinfo"]) +def ig_accinfo(): + result, err = ighelper.request_account_info() + if err: + return jsonify(result), 500 + else: + return jsonify(result), 200 + + +# execute login +@bl_admin_ig.route("/login", methods=["GET"]) +@role_required(["ig.login"]) +def ig_login(): + result, err = ighelper.request_login() + if err: + return jsonify(result), 500 + else: + return jsonify(result), 200 + + +# check queue +@bl_admin_ig.route("/queue", methods=["GET"]) +@role_required(["ig.queue"]) +def ig_queue(): + result = ighelper.request_queue() + return jsonify(result), 200 diff --git a/blueprints/admin/setting.py b/blueprints/admin/setting.py new file mode 100644 index 0000000..8a0280d --- /dev/null +++ b/blueprints/admin/setting.py @@ -0,0 +1,32 @@ +import json + +from flask import Blueprint, request, jsonify, g + +from blueprints.admin.utils import role_required +from utils import setting_loader, logger +from utils.misc import error + +# prefix: /admin/setting +bl_admin_setting = Blueprint("admin_setting", __name__) + +# get settings +@bl_admin_setting.route("/", methods=["GET"]) +@role_required(["setting.edit"]) +def setting_get(): + return jsonify(setting_loader.loadset()), 200 + + +# edit settings +@bl_admin_setting.route("/", methods=["POST"]) +@role_required(["setting.edit"]) +def setting_edit(): + opuser = g.opuser.user + + req = request.json + d = None + for r in req: + d = setting_loader.writeset(r, req.get(r)) + if d == 0: return error("Failed"), 401 + logger.logger("setting.modify", "User:%s modified settings: %s"%(opuser, json.dumps(request.json))) + + return jsonify(d), 200 diff --git a/blueprints/admin/user.py b/blueprints/admin/user.py new file mode 100644 index 0000000..3c6d389 --- /dev/null +++ b/blueprints/admin/user.py @@ -0,0 +1,140 @@ +import os +import time +import math + +import jwt +from flask import Blueprint, request, jsonify, make_response, g +from bcrypt import hashpw, gensalt, checkpw + +from utils import pgclass, setting_loader, logger, dbhelper +from utils.misc import error +from utils.platform_consts import PLIST +from blueprints.admin.utils import role_required + +# prefix: /admin/user +bl_admin_user = Blueprint("admin_user", __name__) + +# login +@bl_admin_user.route("/login", methods=["POST"]) +def login(): + # args + if "username" not in request.json or "password" not in request.json: return error("Arguments error"), 400 + username = str(request.json["username"]) + password = str(request.json["password"]) + + # variables + settings = setting_loader.loadset() + exptime = int(settings["JWT_Valid_Time"]) + + # db + table = pgclass.SQLuser + with dbhelper.db.getsession() as session: + u = session.query(table).filter(table.user==username).first() + # auth + if u is None: return error("Login Failed"), 401 # 找不到用戶 + if not checkpw(password.encode("utf-8"), u.password.encode("utf-8")): return error("Login Failed"), 401 # 密碼沒法跟hash對上 + + # jwt + key = os.getenv("JWT_KEY", None) + if key is None: return error("JWT_KEY error"), 500 + jwtdata = {"id": u.id, "user":username, "exp":int(math.floor(time.time() + exptime))} + jwtdata = jwt.encode(payload = jwtdata, key = str(key), algorithm = "HS256") + + # logger + logger.logger("login", "User:%s logined"%username) + + # cookie + r = make_response("Access Granted") + r.set_cookie("token", jwtdata) + return r, 200 + + +# check who i am +@bl_admin_user.route("/me", methods=["GET"]) +@role_required([]) +def user_me(): + opuser = g.opuser + return jsonify({"id":opuser.id, "user":opuser.user, "permission":opuser.permission}), 200 + + +#################### +# User Area # +#################### +# list users +@bl_admin_user.route("/list", methods={"GET"}) +@role_required([]) +def user_list(): + table = pgclass.SQLuser + with dbhelper.db.getsession() as session: users = session.query(table).all() + res = [ {"id":u.id, "user":u.user, "permission":u.permission} for u in users ] + return jsonify(res), 200 + + +# show an user's info +@bl_admin_user.route("/", methods=["GET"]) +@role_required([]) +def user_get(id:int): + table = pgclass.SQLuser + with dbhelper.db.getsession() as session: u = session.query(table).filter(table.id==int(id)).first() + if u is None: return error("User not found"), 404 + return jsonify({"id":u.id, "user":u.user, "permission":u.permission}), 200 + + +# delete an user +@bl_admin_user.route("/", methods=["DELETE"]) +@role_required(["usermgr"]) +def user_del(id:int): + # db + table = pgclass.SQLuser + + with dbhelper.db.getsession() as session: + opuser = g.opuser # user who requested + + # check root + tguser = session.query(table).filter(table.id==int(id)).first() + if tguser is None: return error("User is not exist"), 400 + if tguser.user == "root": return error("You cannot delete user:root"), 400 + + # delete + session.delete(tguser) + session.commit() + + logger.logger("user.delete", "User:%s deleted an user:%s"%(opuser.user, tguser.user)) # logger + return jsonify({"result":"OK"}), 200 + + +# new user +@bl_admin_user.route("/", methods=["POST"]) +@role_required(["usermgr"]) +def user_add(): + # db + table = pgclass.SQLuser + with dbhelper.db.getsession() as session: + # user who requested + opuser = g.opuser + + # payload + if "username" not in request.json or "password" not in request.json or \ + "permission" not in request.json or not(isinstance(request.json["permission"], list)): + return error("Arguments error"), 400 + username = str(request.json["username"]) + password = str(request.json["password"]) + permission = list(set([ str(p) for p in list(request.json["permission"]) ])) + # check username and password + if username == None or len(username) == 0 or password is None or len(password) == 0: + return error("Invalid Username or Password!"), 400 + # check permission list + for p in permission: + if p not in PLIST: return error("Invalid Permission"), 400 # 如果添加的權限名稱不合法 + if p not in opuser.permission: return error("You don't have the permission: %s"%p), 402 # 如果用戶本身不具有相同權限 + + # add + users = session.query(table).filter(table.user==username).first() + if users is None: # check whether the user already exist + pwhash = hashpw(password.encode("utf-8"), gensalt()).decode("utf-8") + session.add(table(user=username, password=pwhash, permission=permission)) + session.commit() + logger.logger("user.create", "User:%s created a new user:%s"%(opuser.user, username)) # logger + return jsonify({"user":username, "permission":permission}), 200 + else: + return error("User already exist!"), 400 diff --git a/blueprints/admin/utils.py b/blueprints/admin/utils.py new file mode 100644 index 0000000..f1285d9 --- /dev/null +++ b/blueprints/admin/utils.py @@ -0,0 +1,67 @@ +from functools import wraps +import os + +from flask import request, g, abort +import jwt + +from utils.misc import error +from utils.platform_consts import PLIST_ROOT +from utils import pgclass, dbhelper + +# jwt = {"id":user.id, "user":user.user, "exp":time.time} + +# auth decorator +def role_required(permreq: list): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # get data 嘗試解碼jwt + key = os.getenv("JWT_KEY", None) + jwtsession = request.cookies.get("token", None) + if jwtsession == None: # no session + return error("You do not have permission to view this page."), 401 + jwtsession = str(jwtsession) + # decode + try: + jwtdata = jwt.decode(jwt = jwtsession, key = key, algorithms = ["HS256"]) + except jwt.exceptions.ExpiredSignatureError: # token expired + return error("Token expired!"), 401 + except jwt.exceptions.DecodeError: # invalid token + return error("Invalid token!"), 401 + if "id" not in jwtdata or "user" not in jwtdata: # invalid token (struct) + return error("Invalid token!"), 401 + + # db 驗證帳號是否正確 + table = pgclass.SQLuser + with dbhelper.db.getsession() as session: + res = session.query(table).filter(table.user == jwtdata["user"], table.id == jwtdata["id"]).first() + if res is None: # user not found + return error("You do not have permission to view this page."), 401 + + # permission check 確保用戶有此路徑要求的權限 並且權限名稱皆合法 + permissionList = list(set(res.permission)) + for p in permissionList: # 檢查用戶JWT是否有不合法的權限名稱 + if p not in PLIST_ROOT: return error("The user has invalid permission."), 402 + for p in list(set(permreq)): # 檢查要求的權限是否為用戶所持有 + if p not in permissionList: return error("You do not have permission to view this page."), 402 + + # return + g.opuser = res + return f(*args, **kwargs) + return decorated_function + return decorator + + +def check_key(type:str, key:str | int) -> str | int: + if type == 'article': + if not (len(key) > 0 and key.isdigit()): + return abort(400) + outkey = int(key) # id + elif type == 'comment': + if not (len(key) > 0): + return abort(400) + outkey = str(key) # sha1 + else: + return abort(404) + + return outkey \ No newline at end of file diff --git a/blueprints/article.py b/blueprints/general/article.py similarity index 57% rename from blueprints/article.py rename to blueprints/general/article.py index 4ced358..606169c 100644 --- a/blueprints/article.py +++ b/blueprints/general/article.py @@ -6,74 +6,29 @@ from utils import pgclass, setting_loader, dbhelper from utils.misc import internal_json2protobuf, error_proto from protobuf_files import niming_pb2 -article = Blueprint('article', __name__) +# prefix: /article +bl_article = Blueprint('article', __name__) +######################### +# Article # +######################### # 匿名文列表 -@article.route('/list', methods = ["GET"]) +@bl_article.route('/list', methods = ["GET"]) def listing(): res, code = dbhelper.multi_article_fetcher("general", request.args.get("page"), 30) return res, code +# 這裡要改 # 獲取匿名文附檔 -@article.route("/file/", methods=["GET"]) +@bl_article.route("/file/", methods=["GET"]) def getfile(fnhash:str): resp, code = dbhelper.solo_file_fetcher("general", fnhash) return resp, code -# 只有發文者可以看到的獲取指定文章 -# 只有發文者可以做到的刪除文章 -@article.route("/own//", methods = ["GET", "DELETE"]) -def owner_getarticle(type:str, key:str): - # arguments - sha256 = request.args.get("hash", None) - if not sha256: - return abort(400) - sha256 = str(sha256) - - if type == 'a': - if not (len(key) > 0 and key.isdigit()): - return abort(400) - key = int(key) # id - elif type == 'c': - if not (len(key) > 0): - return abort(400) - key = str(key) # sha1 - else: - return abort(404) - - # 獲取指定文章/留言 - if request.method == "GET": - if type == 'a': # 文章 - resfn, code = dbhelper.solo_article_fetcher("owner", key=key, hash=sha256) - elif type == 'c': # 留言 - resfn, code = dbhelper.solo_comment_fetcher("owner", key=key, hash=sha256) - if code == 200: - return internal_json2protobuf(role="owner", otype=type, original=[resfn]), code - return abort(code) - # 刪除指定文章/留言 - elif request.method == "DELETE": - if type == 'a': - rtype = niming_pb2.FetchPostResponse - result, code = dbhelper.solo_article_remover("owner", hash=sha256, id=key) - elif type == 'c': - rtype = niming_pb2.FetchCommentResponse - result, code = dbhelper.solo_comment_remover("owner", hash=sha256, sha1=key) - - if not code == 200: # Exception - return abort(code) - - if type == 'a': - ret = rtype(posts=[ rtype.Message(id=result["id"]) ]) - elif type == 'c': - ret = rtype(posts=[ rtype.Message(sha1=result["sha1"]) ]) - - return ret.SerializeToString(), 200 - - # 獲取指定文章 -@article.route("/a/", methods = ["GET"]) +@bl_article.route("/", methods = ["GET"]) def getarticle(id:int): resfn, code = dbhelper.solo_article_fetcher("general", key=id) if code == 200: @@ -81,17 +36,10 @@ def getarticle(id:int): return abort(code) -# 獲取指定文章的留言 -@article.route("/c/", methods = ["GET"]) -def getcomment(sha1:str): - resfn, code = dbhelper.solo_comment_fetcher("general", key=sha1) - if code == 200: - return internal_json2protobuf(role="general", otype='c', original=[resfn]), code - return abort(code) - - -# 上傳文章 / 留言 -@article.route("/", methods = ["POST"]) +######################### +# Post(Article/Comment) # +######################### +@bl_article.route("/", methods = ["POST"]) def posting(): """ Work Flow: diff --git a/blueprints/general/comment.py b/blueprints/general/comment.py new file mode 100644 index 0000000..4b82f7f --- /dev/null +++ b/blueprints/general/comment.py @@ -0,0 +1,15 @@ +from flask import Blueprint, abort + +from utils import dbhelper +from utils.misc import internal_json2protobuf + +# prefix: /comment +bl_comment = Blueprint('comment', __name__) + +# 獲取指定文章的留言 +@bl_comment.route("/", methods = ["GET"]) +def getcomment(sha1:str): + resfn, code = dbhelper.solo_comment_fetcher("general", key=sha1) + if code == 200: + return internal_json2protobuf(role="general", otype='c', original=[resfn]), code + return abort(code) diff --git a/blueprints/log.py b/blueprints/misc/log.py similarity index 89% rename from blueprints/log.py rename to blueprints/misc/log.py index 26a451c..708186a 100644 --- a/blueprints/log.py +++ b/blueprints/misc/log.py @@ -4,10 +4,11 @@ from sqlalchemy import desc from utils import pgclass, dbhelper from utils.misc import error -log = Blueprint('log', __name__) +# prefix: /log +bl_log = Blueprint('log', __name__) # 列出log -@log.route("/list", methods = ["GET"]) +@bl_log.route("/list", methods = ["GET"]) def listlog(): # variables if request.args.get("start") is None or request.args.get("count") is None or \ @@ -28,7 +29,7 @@ def listlog(): # 指定顯示特定一條log -@log.route("/", methods = ["GET"]) +@bl_log.route("/", methods = ["GET"]) def getlog(id:int): # db with dbhelper.db.getsession() as session: diff --git a/blueprints/owner/owner_article.py b/blueprints/owner/owner_article.py new file mode 100644 index 0000000..c1446c7 --- /dev/null +++ b/blueprints/owner/owner_article.py @@ -0,0 +1,58 @@ +from flask import Blueprint, request, abort + +from utils import dbhelper +from utils.misc import internal_json2protobuf +from protobuf_files import niming_pb2 + +# prefix: /owner/ +bl_owner_article = Blueprint('owner_article', __name__) + + +# 只有發文者可以看到的獲取指定文章 +# 只有發文者可以做到的刪除文章 +@bl_owner_article.route("//", methods = ["GET", "DELETE"]) +def owner_getarticle(type:str, key:str): + # arguments + sha256 = request.args.get("hash", None) + if not sha256: + return abort(400) + sha256 = str(sha256) + + if type == 'article': + if not (len(key) > 0 and key.isdigit()): + return abort(400) + type, key = 'a', int(key) # id + elif type == 'comment': + if not (len(key) > 0): + return abort(400) + type, key = 'c', str(key) # sha1 + else: + return abort(404) + + # 獲取指定文章/留言 + if request.method == "GET": + if type == 'a': # 文章 + resfn, code = dbhelper.solo_article_fetcher("owner", key=key, hash=sha256) + elif type == 'c': # 留言 + resfn, code = dbhelper.solo_comment_fetcher("owner", key=key, hash=sha256) + if code == 200: + return internal_json2protobuf(role="owner", otype=type, original=[resfn]), code + return abort(code) + # 刪除指定文章/留言 + elif request.method == "DELETE": + if type == 'a': + rtype = niming_pb2.FetchPostResponse + result, code = dbhelper.solo_article_remover("owner", hash=sha256, id=key) + elif type == 'c': + rtype = niming_pb2.FetchCommentResponse + result, code = dbhelper.solo_comment_remover("owner", hash=sha256, sha1=key) + + if not code == 200: # Exception + return abort(code) + + if type == 'a': + ret = rtype(posts=[ rtype.Message(id=result["id"]) ]) + elif type == 'c': + ret = rtype(posts=[ rtype.Message(sha1=result["sha1"]) ]) + + return ret.SerializeToString(), 200 diff --git a/requirements.txt b/requirements.txt index 4ed249e..d504a16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ bcrypt pytz sqlalchemy-utils minio -grpcio \ No newline at end of file +grpcio +gunicorn \ No newline at end of file diff --git a/settings.json b/settings.json index 7536d88..73e41af 100644 --- a/settings.json +++ b/settings.json @@ -1 +1 @@ -{"Check_Before_Post": false, "JWT_Valid_Time": 604800, "Niming_Max_Word": 500, "Attachment_Count": 5, "Attachment_Size": 209715200, "Allowed_MIME": ["image/jpeg", "image/pjpeg", "image/png", "image/heic", "image/heif", "video/mp4", "video/quicktime", "video/hevc", "image/webp"]} \ No newline at end of file +{"Check_Before_Post": true, "JWT_Valid_Time": 604800, "Niming_Max_Word": 500, "Attachment_Count": 5, "Attachment_Size": 209715200, "Allowed_MIME": ["image/jpeg", "image/pjpeg", "image/png", "image/heic", "image/heif", "video/mp4", "video/quicktime", "video/hevc", "image/webp"]} \ No newline at end of file diff --git a/utils/dbhelper.py b/utils/dbhelper.py index f010880..94ae474 100644 --- a/utils/dbhelper.py +++ b/utils/dbhelper.py @@ -75,6 +75,7 @@ def solo_article_uploader(content:str, file_list, fmimes:List[str]) -> Tuple[int session.commit() result_id = int(posta.id) except: + session.rollback() return 0, "" # logger @@ -140,6 +141,7 @@ def solo_comment_uploader(content:str, ref:int) -> Tuple[int | str, str]: return sha1, hash except Exception as e: + session.rollback() return 0, ""