diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/frest/auth/__init__.py | 0 | ||||
-rw-r--r-- | src/frest/auth/forms.py | 10 | ||||
-rw-r--r-- | src/frest/auth/models.py | 52 | ||||
-rw-r--r-- | src/frest/auth/routes.py | 183 | ||||
-rw-r--r-- | src/frest/database.py | 4 | ||||
-rw-r--r-- | src/frest/decorators.py | 40 | ||||
-rw-r--r-- | src/frest/forms.py | 20 | ||||
-rw-r--r-- | src/frest/mail.py | 11 | ||||
-rw-r--r-- | src/frest/utils.py | 44 |
9 files changed, 364 insertions, 0 deletions
diff --git a/src/frest/auth/__init__.py b/src/frest/auth/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/frest/auth/__init__.py diff --git a/src/frest/auth/forms.py b/src/frest/auth/forms.py new file mode 100644 index 0000000..abc2f49 --- /dev/null +++ b/src/frest/auth/forms.py @@ -0,0 +1,10 @@ +from .models import User +from forms import ModelForm + + +class UserForm(ModelForm): + model = User + + def __init__(self, data): + super().__init__(self.model) + self.data = data diff --git a/src/frest/auth/models.py b/src/frest/auth/models.py new file mode 100644 index 0000000..ea79def --- /dev/null +++ b/src/frest/auth/models.py @@ -0,0 +1,52 @@ +from database import db +from datetime import datetime +import string +import random +from hashlib import sha256 +from pytz import timezone +import os + + +def generate_token(): + chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + return "".join(random.choice(chars) for _ in range(18)) + + +class User(db.Model): + userId = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(30)) + password = db.Column(db.String(30)) + is_admin = db.Column(db.Boolean, default=False) + name = db.Column(db.String(30)) + created_at = db.Column(db.DateTime) + + def __init__(self, **kwargs): + self.email = kwargs.get("email") + psw_hash = sha256(kwargs.get("password").encode()) + self.password = psw_hash.hexdigest() + self.name = kwargs.get("name") + self.is_admin = kwargs.get("is_admin") + self.created_at = datetime.now( + timezone(os.getenv("FREST_TIMEZONE", "Europe/Rome")) + ) + + def __repr__(self): + return f"<User '{self.userId}'>" + + +class Token(db.Model): + tokenId = db.Column(db.Integer, primary_key=True) + string = db.Column(db.String(20)) + created_at = db.Column(db.DateTime) + expired = db.Column(db.Boolean) + user_id = db.Column(db.Integer, db.ForeignKey("user.userId"), nullable=False) + user = db.relationship("User", backref=db.backref("tokens", lazy=True)) + + def __init__(self, user): + self.user = user + self.string = f"{generate_token()}==" + self.created_at = datetime.utcnow() + self.expired = False + + def __repr__(self): + return f"<Token '{self.string}'>" diff --git a/src/frest/auth/routes.py b/src/frest/auth/routes.py new file mode 100644 index 0000000..c4dcfc9 --- /dev/null +++ b/src/frest/auth/routes.py @@ -0,0 +1,183 @@ +from flask import Blueprint, request, abort +from utils import http_call, model_serialize +from decorators import check_token, admin_required +from .models import User, Token +from .forms import UserForm +from database import db +from hashlib import sha256 +from sqlalchemy import desc + +api = Blueprint("users", __name__) + + +@api.route("/api/login", methods=["POST"]) +def login(): + if not request.json: + abort(400) + + data = request.json + + auth = request.headers.get("Authentication") + if auth: + t = Token.query.filter_by(string=auth).first() + if not t: + abort(404) + + if t.user.is_admin: + return http_call( + {"userId": t.user.userId, "login": True, "token": t.string}, 200 + ) + else: + abort(403) + + if "email" in data and "password" in data: + psw_hash = sha256(data["password"].encode()) + data["password"] = psw_hash.hexdigest() + u = User.query.filter_by(email=data["email"], password=data["password"]).first() + + if not u: + abort(404) + + if "is_admin" in data: + if u.is_admin == 0: + abort(403) + + last_token = ( + Token.query.filter_by(user=u).order_by(desc(Token.tokenId)).all()[-1] + ) + last_token.expired = True + + t = Token(user=u) + + db.session.add(t) + db.session.commit() + + return http_call({"userId": u.userId, "login": True, "token": t.string}, 200) + + abort(404) + + +@api.route("/api/user/hash_password", methods=["GET"]) +def hash_password_exists(): + data = request.args + if not data.get("hash_password"): + abort(400) + + if User.query.filter_by(password=data["hash_password"]): + return http_call({}, 200) + + return http_call({}, 404) + + +@api.route("/api/user/new-password/<alias>", methods=["PUT"]) +def new_user_password(alias): + data = request.json + if not data.get("password"): + abort(400) + + u = User.query.filter_by(password=alias).first() + + if not u: + abort(404) + + u.password = sha256(data["password"].encode()).hexdigest() + db.session.commit() + + return http_call({}, 200) + + +@api.route("/api/user", methods=["POST"]) +def new_user(): + if not request.json: + abort(400) + + form = UserForm(request.json) + + if not form.get("is_admin") or form.is_valid(): + if User.query.filter_by(email=form.get("email")).first(): + abort(400) + + u = User( + email=form.get("email"), + password=form.get("password"), + name=form.get("name"), + is_admin=form.get("is_admin"), + ) + t = Token(user=u) + db.session.add(u) + db.session.add(t) + + db.session.commit() + + return http_call({"userId": u.userId, "token": t.string}, 201) + + abort(400) + + +@api.route("/api/users") +@check_token +@admin_required +def all_users(): + return http_call( + [ + model_serialize(i, params="userId,email,is_admin,name,created_at") + for i in User.query.all() + ], + 200, + ) + + +@api.route("/api/user/<int:userId>") +@check_token +def get_user(userId): + return http_call( + model_serialize( + User.query.filter_by(userId=userId).first(), + params="userId,email,is_admin,name,created_at", + ), + 200, + ) + + +@api.route("/api/user/<userId>", methods=["DELETE"]) +@check_token +def delete_user(userId): + u = User.query.filter_by(userId=userId) + if not u: + abort(404) + + deleted = u.delete() + db.session.commit() + + return http_call({"delete": deleted}, 200) + + +@api.route("/api/user/<userId>", methods=["PUT"]) +@check_token +def edit_user(userId): + if not request.json: + abort(400) + + form = UserForm(request.json) + u = User.query.filter_by(userId=userId).first() + if not u: + abort(400) + + if form.get("password"): + psw = True + else: + psw = False + + if not psw or not form.get("is_admin") or form.is_valid(): + u.name = form.get("name") + u.email = form.get("email") + u.is_admin = form.get("is_admin") + if psw: + crypt_psw = sha256(form.get("password").encode()).hexdigest() + u.password = crypt_psw + + db.session.commit() + + return http_call({"userId": u.userId}, 200) + + abort(400) diff --git a/src/frest/database.py b/src/frest/database.py new file mode 100644 index 0000000..176cd52 --- /dev/null +++ b/src/frest/database.py @@ -0,0 +1,4 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() +config = {"DATABASE_URI": "sqlite:///database.db"} diff --git a/src/frest/decorators.py b/src/frest/decorators.py new file mode 100644 index 0000000..181b62d --- /dev/null +++ b/src/frest/decorators.py @@ -0,0 +1,40 @@ +from flask import request, abort +from auth.models import Token +from functools import wraps + + +def check_token(f): + @wraps(f) + def inner(*args, **kwargs): + userid = request.url.split('/')[-1] + headers = request.headers + if not headers.get("Authentication"): + abort(403) + + auth = request.headers.get("Authentication") + token = Token.query.filter_by(string=auth).first() + if not token: + abort(403) + + if userid.isdigit(): + if int(userid) != token.user.userId and not token.user.is_admin: + abort(403) + + return f(*args, **kwargs) + + return inner + + +def admin_required(f): + @wraps(f) + def inner(*args, **kwargs): + header = request.headers + + auth = request.headers.get("Authentication") + token = Token.query.filter_by(string=auth).first() + if not token.user.is_admin: + abort(403) + + return f(*args, **kwargs) + + return inner diff --git a/src/frest/forms.py b/src/frest/forms.py new file mode 100644 index 0000000..8ce2e95 --- /dev/null +++ b/src/frest/forms.py @@ -0,0 +1,20 @@ +class ModelForm(object): + def __init__(self, model): + self.data = {} + self.model = model + no_params = ["metadata", "query", "query_class"] + self.attributes = [ + i for i in dir(self.model) if not i.startswith("_") and i not in no_params + ] + self.ignore = [] + + def is_valid(self): + for key, value in self.data.items(): + if key in self.attributes: + if (value == "" or not value) and key not in self.ignore: + return False + + return True + + def get(self, attr): + return self.data.get(attr) diff --git a/src/frest/mail.py b/src/frest/mail.py new file mode 100644 index 0000000..e553525 --- /dev/null +++ b/src/frest/mail.py @@ -0,0 +1,11 @@ +from flask_mail import Mail + +mail = Mail() +config = { + "SERVER": "", + "PORT": 587, + "USE_TLS": True, + "USERNAME": "", + "DEFAULT_SENDER": "", + "PASSWORD": "", +} diff --git a/src/frest/utils.py b/src/frest/utils.py new file mode 100644 index 0000000..934aec4 --- /dev/null +++ b/src/frest/utils.py @@ -0,0 +1,44 @@ +from flask import make_response, jsonify, request, render_template +from flask_mail import Message +from mail import mail +from datetime import datetime +import os + + +def send_email(sender, email, activation_code, title, template): + msg = Message(title, sender=sender, recipients=[email]) + rest_link = os.getenv("FREST_URL", "http://localhost:8080/app") + msg.html = render_template(template, link=rest_link, code=activation_code) + mail.send(msg) + + +def http_call(data, status): + return make_response(jsonify({"status": status, "result": data}), status) + + +def model_serialize(obj, params="", extend_model_for=[]): + fields = {} + params = params.split(",") + + for i in [f for f in dir(obj) if f in params]: + if isinstance(obj.__getattribute__(i), datetime): + fields[i] = obj.__getattribute__(i).strftime("%d/%m/%Y %H:%M") + else: + fields[i] = obj.__getattribute__(i) + + if len(extend_model_for) > 0: + for key, value in fields.items(): + if isinstance(value, list): + _l = [] + for v in value: + for i in extend_model_for: + if isinstance(v, i): + _l.append(v.as_json()) + + fields[key] = _l + else: + for i in extend_model_for: + if isinstance(value, i): + fields[key] = value.as_json() + + return fields |