route change
This commit is contained in:
parent
9c26a5610c
commit
50f35fb647
34
app.py
34
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")
|
||||
|
@ -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/<int:id>", 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/<int:id>", 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/<fnhash>", 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/<type>/<key>", 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/<type>/<key>", 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/<type>/<key>", 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
|
109
blueprints/admin/article.py
Normal file
109
blueprints/admin/article.py
Normal file
@ -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/<fnhash>", 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("/<type>/<key>", 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("/<type>/<key>", 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("/<type>/<key>", 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)
|
36
blueprints/admin/ig.py
Normal file
36
blueprints/admin/ig.py
Normal file
@ -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
|
32
blueprints/admin/setting.py
Normal file
32
blueprints/admin/setting.py
Normal file
@ -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
|
140
blueprints/admin/user.py
Normal file
140
blueprints/admin/user.py
Normal file
@ -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("/<int:id>", 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("/<int:id>", 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
|
67
blueprints/admin/utils.py
Normal file
67
blueprints/admin/utils.py
Normal file
@ -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
|
@ -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/<fnhash>", methods=["GET"])
|
||||
@bl_article.route("/file/<fnhash>", methods=["GET"])
|
||||
def getfile(fnhash:str):
|
||||
resp, code = dbhelper.solo_file_fetcher("general", fnhash)
|
||||
return resp, code
|
||||
|
||||
|
||||
# 只有發文者可以看到的獲取指定文章
|
||||
# 只有發文者可以做到的刪除文章
|
||||
@article.route("/own/<type>/<key>", 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/<int:id>", methods = ["GET"])
|
||||
@bl_article.route("/<int:id>", 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/<sha1>", 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:
|
15
blueprints/general/comment.py
Normal file
15
blueprints/general/comment.py
Normal file
@ -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("/<sha1>", 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)
|
@ -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("/<int:id>", methods = ["GET"])
|
||||
@bl_log.route("/<int:id>", methods = ["GET"])
|
||||
def getlog(id:int):
|
||||
# db
|
||||
with dbhelper.db.getsession() as session:
|
58
blueprints/owner/owner_article.py
Normal file
58
blueprints/owner/owner_article.py
Normal file
@ -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("/<type>/<key>", 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
|
@ -9,3 +9,4 @@ pytz
|
||||
sqlalchemy-utils
|
||||
minio
|
||||
grpcio
|
||||
gunicorn
|
@ -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"]}
|
||||
{"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"]}
|
@ -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, ""
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user