Some more on autodetection API interfaces for frontend
* Breaks API for Session and User
This commit is contained in:
parent
28b202cf30
commit
addfb7c7c4
|
@ -12,6 +12,7 @@ from flaschengeist import logger
|
||||||
from flaschengeist.modules import Plugin
|
from flaschengeist.modules import Plugin
|
||||||
from flaschengeist.system.decorator import login_required
|
from flaschengeist.system.decorator import login_required
|
||||||
from flaschengeist.system.controller import sessionController, userController, messageController
|
from flaschengeist.system.controller import sessionController, userController, messageController
|
||||||
|
from flaschengeist.system.models.session import Session
|
||||||
|
|
||||||
session_controller = LocalProxy(lambda: sessionController.SessionController())
|
session_controller = LocalProxy(lambda: sessionController.SessionController())
|
||||||
auth_bp = Blueprint("auth", __name__)
|
auth_bp = Blueprint("auth", __name__)
|
||||||
|
@ -21,6 +22,7 @@ class AuthRoutePlugin(Plugin):
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
super().__init__(blueprint=auth_bp)
|
super().__init__(blueprint=auth_bp)
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
# Routes #
|
# Routes #
|
||||||
# #
|
# #
|
||||||
|
@ -60,21 +62,21 @@ def _login():
|
||||||
|
|
||||||
# Lets cleanup the DB
|
# Lets cleanup the DB
|
||||||
session_controller.clear_expired()
|
session_controller.clear_expired()
|
||||||
return jsonify({"session": session, "permissions": user.get_permissions()})
|
return jsonify({"session": session, "user": user})
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth", methods=["GET"])
|
@auth_bp.route("/auth", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def _get_tokens(access_token, **kwargs):
|
def _get_sessions(access_token: Session, **kwargs):
|
||||||
tokens = session_controller.get_users_sessions(access_token.user)
|
tokens = session_controller.get_users_sessions(access_token._user)
|
||||||
a = messageController.Message(access_token.user, "Go", "Bar")
|
a = messageController.Message(access_token._user, "Go", "Bar")
|
||||||
messageController.send_message(a)
|
messageController.send_message(a)
|
||||||
return jsonify(tokens)
|
return jsonify(tokens)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth/<token>", methods=["DELETE"])
|
@auth_bp.route("/auth/<token>", methods=["DELETE"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def _delete_token(access_token, token, **kwargs):
|
def _delete_session(access_token, token, **kwargs):
|
||||||
logger.debug("Try to delete access token {{ {} }}".format(token))
|
logger.debug("Try to delete access token {{ {} }}".format(token))
|
||||||
token = session_controller.get_session(token, access_token.user)
|
token = session_controller.get_session(token, access_token.user)
|
||||||
if not token:
|
if not token:
|
||||||
|
@ -89,14 +91,26 @@ def _delete_token(access_token, token, **kwargs):
|
||||||
|
|
||||||
@auth_bp.route("/auth/<token>", methods=["GET"])
|
@auth_bp.route("/auth/<token>", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def _get_token(token, access_token, **kwargs):
|
def _get_session(token, access_token, **kwargs):
|
||||||
logger.debug("get token {{ {} }}".format(token))
|
logger.debug("get token {{ {} }}".format(token))
|
||||||
session = session_controller.get_session(token, access_token.user)
|
session = session_controller.get_session(token, access_token.user)
|
||||||
if not token:
|
if not token:
|
||||||
# Return 403 error, so that users can not bruteforce tokens
|
# Return 403 error, so that users can not bruteforce tokens
|
||||||
# Valid tokens from other users and invalid tokens now are looking the same
|
# Valid tokens from other users and invalid tokens now are looking the same
|
||||||
raise Forbidden
|
raise Forbidden
|
||||||
return jsonify({"session": session, "permissions": session.user.get_permissions()})
|
return jsonify(session)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/auth/<token>/user", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def _get_assocd_user(token, access_token, **kwargs):
|
||||||
|
logger.debug("get token {{ {} }}".format(token))
|
||||||
|
session = session_controller.get_session(token, access_token.user)
|
||||||
|
if not token:
|
||||||
|
# Return 403 error, so that users can not bruteforce tokens
|
||||||
|
# Valid tokens from other users and invalid tokens now are looking the same
|
||||||
|
raise Forbidden
|
||||||
|
return jsonify(session._user)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth/<token>", methods=["PUT"])
|
@auth_bp.route("/auth/<token>", methods=["PUT"])
|
||||||
|
|
|
@ -12,6 +12,7 @@ class RolesPlugin(Plugin):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
super().__init__(config, roles_bp)
|
super().__init__(config, roles_bp)
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
# Routes #
|
# Routes #
|
||||||
# #
|
# #
|
||||||
|
|
|
@ -16,6 +16,7 @@ class SchedulePlugin(Plugin):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
super().__init__(blueprint=schedule_bp)
|
super().__init__(blueprint=schedule_bp)
|
||||||
|
|
||||||
|
|
||||||
####################################################################################
|
####################################################################################
|
||||||
# Routes #
|
# Routes #
|
||||||
# #
|
# #
|
||||||
|
|
|
@ -62,7 +62,11 @@ class SessionController(metaclass=Singleton):
|
||||||
logger.debug("create access token")
|
logger.debug("create access token")
|
||||||
token_str = secrets.token_hex(16)
|
token_str = secrets.token_hex(16)
|
||||||
session = Session(
|
session = Session(
|
||||||
token=token_str, user=user, lifetime=self.lifetime, browser=user_agent.browser, platform=user_agent.platform
|
token=token_str,
|
||||||
|
_user=user,
|
||||||
|
lifetime=self.lifetime,
|
||||||
|
browser=user_agent.browser,
|
||||||
|
platform=user_agent.platform,
|
||||||
)
|
)
|
||||||
session.refresh()
|
session.refresh()
|
||||||
db.session.add(session)
|
db.session.add(session)
|
||||||
|
@ -89,7 +93,7 @@ class SessionController(metaclass=Singleton):
|
||||||
return session
|
return session
|
||||||
|
|
||||||
def get_users_sessions(self, user):
|
def get_users_sessions(self, user):
|
||||||
return Session.query.filter(Session.user == user)
|
return Session.query.filter(Session._user == user)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_session(token: Session):
|
def delete_session(token: Session):
|
||||||
|
|
|
@ -19,6 +19,8 @@ def login_user(username, password):
|
||||||
|
|
||||||
def update_user(user):
|
def update_user(user):
|
||||||
current_app.config["FG_AUTH_BACKEND"].update_user(user)
|
current_app.config["FG_AUTH_BACKEND"].update_user(user)
|
||||||
|
if not user.display_name:
|
||||||
|
user.display_name = user.firstname[0] + user.lastname
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,4 +17,4 @@ class HookCall(object):
|
||||||
|
|
||||||
def __call__(self, function):
|
def __call__(self, function):
|
||||||
_hook_dict[self.name] = function
|
_hook_dict[self.name] = function
|
||||||
return function
|
return function
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
class ModelSerializeMixin:
|
||||||
|
def serialize(self):
|
||||||
|
d = {param: getattr(self, param) for param in self.__class__.__annotations__ if not param.startswith("_")}
|
||||||
|
if len(d) == 1:
|
||||||
|
key, value = d.popitem()
|
||||||
|
return value
|
||||||
|
return d
|
|
@ -1,12 +1,13 @@
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from . import ModelSerializeMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
from ..database import db
|
from ..database import db
|
||||||
from secrets import compare_digest
|
from secrets import compare_digest
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
|
|
||||||
|
|
||||||
class Session(db.Model):
|
class Session(db.Model, ModelSerializeMixin):
|
||||||
"""Model for a Session
|
"""Model for a Session
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -16,16 +17,16 @@ class Session(db.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "session"
|
__tablename__ = "session"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
|
||||||
user: User = db.relationship("User", back_populates="sessions")
|
|
||||||
|
|
||||||
expires: datetime = db.Column(db.DateTime)
|
expires: datetime = db.Column(db.DateTime)
|
||||||
token: str = db.Column(db.String(32), unique=True)
|
token: str = db.Column(db.String(32), unique=True)
|
||||||
lifetime: int = db.Column(db.Integer)
|
lifetime: int = db.Column(db.Integer)
|
||||||
browser: str = db.Column(db.String(30))
|
browser: str = db.Column(db.String(30))
|
||||||
platform: str = db.Column(db.String(30))
|
platform: str = db.Column(db.String(30))
|
||||||
|
|
||||||
|
_id = db.Column("id", db.Integer, primary_key=True)
|
||||||
|
_user: User = db.relationship("User", back_populates="_sessions")
|
||||||
|
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"))
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Update the Timestamp
|
"""Update the Timestamp
|
||||||
|
|
||||||
|
@ -33,21 +34,7 @@ class Session(db.Model):
|
||||||
"""
|
"""
|
||||||
logger.debug("update timestamp from session with token {{ {} }}".format(self))
|
logger.debug("update timestamp from session with token {{ {} }}".format(self))
|
||||||
self.expires = datetime.utcnow() + timedelta(seconds=self.lifetime)
|
self.expires = datetime.utcnow() + timedelta(seconds=self.lifetime)
|
||||||
|
self.expires.replace(tzinfo=timezone.utc)
|
||||||
def serialize(self):
|
|
||||||
"""Create Dic to dump in JSON
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A Dic with static Attributes.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"token": self.token,
|
|
||||||
"expires": self.expires.replace(tzinfo=timezone.utc),
|
|
||||||
"lifetime": self.lifetime,
|
|
||||||
"user": self.user,
|
|
||||||
"browser": self.browser,
|
|
||||||
"platform": self.platform,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __eq__(self, token):
|
def __eq__(self, token):
|
||||||
return compare_digest(self.token, token)
|
return compare_digest(self.token, token)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from ..database import db
|
from typing import List
|
||||||
from sqlalchemy.orm.collections import attribute_mapped_collection
|
|
||||||
from flask import current_app
|
|
||||||
from werkzeug.local import LocalProxy
|
|
||||||
|
|
||||||
logger = LocalProxy(lambda: current_app.logger)
|
from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||||
|
|
||||||
|
from . import ModelSerializeMixin
|
||||||
|
from ..database import db
|
||||||
|
from ... import logger
|
||||||
|
|
||||||
association_table = db.Table(
|
association_table = db.Table(
|
||||||
"user_x_role",
|
"user_x_role",
|
||||||
|
@ -11,8 +12,31 @@ association_table = db.Table(
|
||||||
db.Column("role_id", db.Integer, db.ForeignKey("role.id")),
|
db.Column("role_id", db.Integer, db.ForeignKey("role.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
role_permission_association_table = db.Table(
|
||||||
|
"role_x_permission",
|
||||||
|
db.Column("role_id", db.Integer, db.ForeignKey("role.id")),
|
||||||
|
db.Column("permission_id", db.Integer, db.ForeignKey("permission.id")),
|
||||||
|
)
|
||||||
|
|
||||||
class User(db.Model):
|
|
||||||
|
class Permission(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = "permission"
|
||||||
|
name: str = db.Column(db.String(30), unique=True)
|
||||||
|
|
||||||
|
_id = db.Column("id", db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Role(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = "role"
|
||||||
|
name: str = db.Column(db.String(30), unique=True)
|
||||||
|
permissions: [Permission] = db.relationship(
|
||||||
|
"Permission", secondary=role_permission_association_table, cascade="all, delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
_id = db.Column("id", db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class User(db.Model, ModelSerializeMixin):
|
||||||
"""Database Object for User
|
"""Database Object for User
|
||||||
|
|
||||||
Table for all saved User
|
Table for all saved User
|
||||||
|
@ -27,23 +51,24 @@ class User(db.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "user"
|
__tablename__ = "user"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
userid: str = db.Column(db.String(30))
|
||||||
userid = db.Column(db.String(30))
|
display_name: str = db.Column(db.String(30))
|
||||||
display_name = db.Column(db.String(30))
|
firstname: str = db.Column(db.String(30))
|
||||||
firstname = db.Column(db.String(30))
|
lastname: str = db.Column(db.String(30))
|
||||||
lastname = db.Column(db.String(30))
|
mail: str = db.Column(db.String(30))
|
||||||
mail = db.Column(db.String(30))
|
roles: [Role] = db.relationship("Role", secondary=association_table)
|
||||||
roles = db.relationship("Role", secondary=association_table)
|
|
||||||
sessions = db.relationship("Session", back_populates="user")
|
_id = db.Column("id", db.Integer, primary_key=True)
|
||||||
attributes = db.relationship(
|
_sessions = db.relationship("Session", back_populates="_user")
|
||||||
"UserAttribute", collection_class=attribute_mapped_collection("name"), cascade="all, delete"
|
_attributes = db.relationship(
|
||||||
|
"_UserAttribute", collection_class=attribute_mapped_collection("name"), cascade="all, delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_attribute(self, name, value):
|
def set_attribute(self, name, value):
|
||||||
if name in self.attributes:
|
if name in self._attributes:
|
||||||
self.attributes[name].value = value
|
self._attributes[name].value = value
|
||||||
else:
|
else:
|
||||||
self.attributes[name] = UserAttribute(name=name, value=value)
|
self._attributes[name] = _UserAttribute(name=name, value=value)
|
||||||
|
|
||||||
def add_role(self, name):
|
def add_role(self, name):
|
||||||
r = Role.query.filter_by(name=name).first()
|
r = Role.query.filter_by(name=name).first()
|
||||||
|
@ -74,46 +99,10 @@ class User(db.Model):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
return {
|
|
||||||
"userid": self.userid,
|
|
||||||
"display_name": self.display_name,
|
|
||||||
"firstname": self.firstname,
|
|
||||||
"lastname": self.lastname,
|
|
||||||
"mail": self.mail,
|
|
||||||
"roles": [r.name for r in self.roles],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class _UserAttribute(db.Model, ModelSerializeMixin):
|
||||||
class UserAttribute(db.Model):
|
|
||||||
__tablename__ = "user_attribute"
|
__tablename__ = "user_attribute"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column("id", db.Integer, primary_key=True)
|
||||||
user = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
user: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
name = db.Column(db.String(30))
|
name: str = db.Column(db.String(30))
|
||||||
value = db.Column(db.String(192))
|
value: str = db.Column(db.String(192))
|
||||||
|
|
||||||
|
|
||||||
role_permission_association_table = db.Table(
|
|
||||||
"role_x_permission",
|
|
||||||
db.Column("role_id", db.Integer, db.ForeignKey("role.id")),
|
|
||||||
db.Column("permission_id", db.Integer, db.ForeignKey("permission.id")),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Role(db.Model):
|
|
||||||
__tablename__ = "role"
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
name = db.Column(db.String(30), unique=True)
|
|
||||||
permissions = db.relationship("Permission", secondary=role_permission_association_table, cascade="all, delete")
|
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Permission(db.Model):
|
|
||||||
__tablename__ = "permission"
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
name = db.Column(db.String(30), unique=True)
|
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
return self.name
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
import inspect
|
import inspect
|
||||||
import argparse
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import bjoern
|
import bjoern
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from sqlalchemy.orm import RelationshipProperty, ColumnProperty
|
from sqlalchemy.orm import RelationshipProperty, ColumnProperty
|
||||||
|
@ -27,47 +29,53 @@ def run(arguments):
|
||||||
|
|
||||||
def export(arguments):
|
def export(arguments):
|
||||||
import flaschengeist.system.models as models
|
import flaschengeist.system.models as models
|
||||||
|
|
||||||
known = []
|
known = []
|
||||||
done = []
|
done = []
|
||||||
|
classes = {}
|
||||||
|
|
||||||
def orm_type(attr):
|
def pytype(cls):
|
||||||
if hasattr(attr, 'type'):
|
if isinstance(cls, list):
|
||||||
tt = attr.type
|
return "Array<{}>".format(pytype(cls[0]))
|
||||||
if isinstance(attr, ColumnProperty):
|
mapper = {"str": "string", "int": "number", "datetime": "Date"}
|
||||||
tt = attr.columns[0].type
|
if cls.__name__ in mapper:
|
||||||
elif isinstance(attr, RelationshipProperty):
|
return mapper[cls.__name__]
|
||||||
proto = "{}"
|
|
||||||
if attr.key.endswith("s"):
|
|
||||||
proto = "Array<{}>"
|
|
||||||
return proto.format(attr.mapper.class_.__name__)
|
|
||||||
else:
|
else:
|
||||||
raise TypeError("Couldn't inspect type.")
|
return cls.__name__
|
||||||
tt = tt.__str__().split("(")[0]
|
|
||||||
return {"INTEGER": "number",
|
|
||||||
"VARCHAR": "string",
|
|
||||||
"DATETIME": "Date"}[tt]
|
|
||||||
|
|
||||||
def walker(mod, file):
|
def walker(mod):
|
||||||
if inspect.isclass(mod[1]) and mod[1].__module__.startswith(models.__name__) and mod[0] not in done:
|
if inspect.ismodule(mod[1]) and mod[1].__name__.startswith(models.__name__) and mod[1].__name__ not in known:
|
||||||
mapper = sqlalchemy.inspect(mod[1], False)
|
|
||||||
if mapper is not None:
|
|
||||||
file.write("interface {} {{\n".format(mod[0]))
|
|
||||||
for desc in mapper.attrs:
|
|
||||||
file.write(" {}: {};\n".format(desc.key, orm_type(desc)))
|
|
||||||
file.write("}\n")
|
|
||||||
done.append(mod[0])
|
|
||||||
|
|
||||||
elif inspect.ismodule(mod[1]) and mod[1].__name__.startswith(models.__name__) and mod[1].__name__ not in known:
|
|
||||||
known.append(mod[1].__name__)
|
known.append(mod[1].__name__)
|
||||||
for cls in inspect.getmembers(mod[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)):
|
for cls in inspect.getmembers(mod[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)):
|
||||||
walker(cls, file)
|
walker(cls)
|
||||||
|
elif (
|
||||||
|
inspect.isclass(mod[1])
|
||||||
|
and mod[1].__module__.startswith(models.__name__)
|
||||||
|
and mod[0] not in classes
|
||||||
|
and not mod[0].startswith("_")
|
||||||
|
and hasattr(mod[1], "__annotations__")
|
||||||
|
):
|
||||||
|
d = {param: pytype(ptype) for param, ptype in mod[1].__annotations__.items() if not param.startswith("_")}
|
||||||
|
if len(d) == 1:
|
||||||
|
key, value = d.popitem()
|
||||||
|
classes[mod[0]] = value
|
||||||
|
else:
|
||||||
|
classes[mod[0]] = d
|
||||||
|
|
||||||
from flaschengeist.app import create_app
|
from flaschengeist.app import create_app
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
walker(("models", models))
|
||||||
with open(arguments.file, "w") as file:
|
with open(arguments.file, "w") as file:
|
||||||
walker(("models", models), file)
|
for cls, params in classes.items():
|
||||||
|
if isinstance(params, str):
|
||||||
|
file.write("type {} = {};\n".format(cls, params))
|
||||||
|
else:
|
||||||
|
file.write("interface {} {{\n".format(cls))
|
||||||
|
for name in params:
|
||||||
|
file.write(" {}: {};\n".format(name, params[name]))
|
||||||
|
file.write("}\n")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -88,6 +96,5 @@ if __name__ == "__main__":
|
||||||
parser_export.set_defaults(func=export)
|
parser_export.set_defaults(func=export)
|
||||||
parser_export.add_argument("--file", help="Filename where to save", default="flaschengeist.d.ts")
|
parser_export.add_argument("--file", help="Filename where to save", default="flaschengeist.d.ts")
|
||||||
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
Loading…
Reference in New Issue