From 3a2246e26e9febe3c15e2ddc1e7e6f320f86fe15 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Mon, 6 Apr 2020 21:54:53 +0200 Subject: chore: move package in frest folder --- frest/__init__.py | 4 + frest/app.py | 57 +++++++++++ frest/auth/__init__.py | 0 frest/auth/forms.py | 10 ++ frest/auth/models.py | 52 ++++++++++ frest/auth/routes.py | 183 +++++++++++++++++++++++++++++++++ frest/database.py | 4 + frest/decorators.py | 40 ++++++++ frest/forms.py | 20 ++++ frest/mail.py | 11 ++ frest/manage/__init__.py | 0 frest/manage/bcolors.py | 17 +++ frest/manage/utils.py | 228 +++++++++++++++++++++++++++++++++++++++++ frest/templates/form.txt | 11 ++ frest/templates/models.txt | 26 +++++ frest/templates/routes.txt | 88 ++++++++++++++++ frest/utils.py | 44 ++++++++ frest/wsgi.py | 4 + src/frest/auth/__init__.py | 0 src/frest/auth/forms.py | 10 -- src/frest/auth/models.py | 52 ---------- src/frest/auth/routes.py | 183 --------------------------------- src/frest/database.py | 4 - src/frest/decorators.py | 40 -------- src/frest/forms.py | 20 ---- src/frest/mail.py | 11 -- src/frest/manage.py | 23 ----- src/frest/manage/__init__.py | 0 src/frest/manage/bcolors.py | 17 --- src/frest/manage/utils.py | 226 ---------------------------------------- src/frest/templates/form.txt | 11 -- src/frest/templates/models.txt | 26 ----- src/frest/templates/routes.txt | 88 ---------------- src/frest/utils.py | 44 -------- 34 files changed, 799 insertions(+), 755 deletions(-) create mode 100644 frest/__init__.py create mode 100644 frest/app.py create mode 100644 frest/auth/__init__.py create mode 100644 frest/auth/forms.py create mode 100644 frest/auth/models.py create mode 100644 frest/auth/routes.py create mode 100644 frest/database.py create mode 100644 frest/decorators.py create mode 100644 frest/forms.py create mode 100644 frest/mail.py create mode 100644 frest/manage/__init__.py create mode 100644 frest/manage/bcolors.py create mode 100644 frest/manage/utils.py create mode 100644 frest/templates/form.txt create mode 100644 frest/templates/models.txt create mode 100644 frest/templates/routes.txt create mode 100644 frest/utils.py create mode 100644 frest/wsgi.py delete mode 100644 src/frest/auth/__init__.py delete mode 100644 src/frest/auth/forms.py delete mode 100644 src/frest/auth/models.py delete mode 100644 src/frest/auth/routes.py delete mode 100644 src/frest/database.py delete mode 100644 src/frest/decorators.py delete mode 100644 src/frest/forms.py delete mode 100644 src/frest/mail.py delete mode 100644 src/frest/manage.py delete mode 100644 src/frest/manage/__init__.py delete mode 100644 src/frest/manage/bcolors.py delete mode 100644 src/frest/manage/utils.py delete mode 100644 src/frest/templates/form.txt delete mode 100644 src/frest/templates/models.txt delete mode 100644 src/frest/templates/routes.txt delete mode 100644 src/frest/utils.py diff --git a/frest/__init__.py b/frest/__init__.py new file mode 100644 index 0000000..dda6153 --- /dev/null +++ b/frest/__init__.py @@ -0,0 +1,4 @@ +from .forms import * +from .utils import * +from .mail import * +from .database import * diff --git a/frest/app.py b/frest/app.py new file mode 100644 index 0000000..ca46277 --- /dev/null +++ b/frest/app.py @@ -0,0 +1,57 @@ +from flask import Flask +from auth.routes import api as api_users +from flask import make_response, jsonify +from database import db +from database import config as db_config +from mail import mail +from mail import config as mail_config +from flask_sqlalchemy import SQLAlchemy +from utils import http_call +from flask_cors import CORS +import os + +app = Flask(__name__) +app.config["SQLALCHEMY_DATABASE_URI"] = db_config["DATABASE_URI"] +app.config["DEBUG"] = os.getenv("FREST_DEBUG", True) +app.config["CORS_HEADERS"] = "Content-Type" +app.config["MAIL_SERVER"] = mail_config["SERVER"] +app.config["MAIL_PORT"] = mail_config["PORT"] +app.config["MAIL_USE_TLS"] = mail_config["USE_TLS"] +app.config["MAIL_USERNAME"] = mail_config["USERNAME"] +app.config["MAIL_DEFAULT_SENDER"] = mail_config["DEFAULT_SENDER"] +app.config["MAIL_PASSWORD"] = mail_config["PASSWORD"] + +cors = CORS(app, resources={r"/.*": {"origins": "*"}}) +db.app = app +db.init_app(app) +mail.init_app(app) +app.register_blueprint(api_users) + + +@app.errorhandler(404) +def not_found(error): + return http_call("Not found", 404) + + +@app.errorhandler(400) +def bad_request(error): + return http_call("Bad request", 400) + + +@app.errorhandler(405) +def method_not_allowed(error): + return http_call("Method not allowed", 405) + + +@app.errorhandler(403) +def forbiddend(error): + return http_call("Forbidden", 403) + + +@app.errorhandler(500) +def internal(error): + return http_call("Internal error", 500) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/frest/auth/__init__.py b/frest/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frest/auth/forms.py b/frest/auth/forms.py new file mode 100644 index 0000000..abc2f49 --- /dev/null +++ b/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/frest/auth/models.py b/frest/auth/models.py new file mode 100644 index 0000000..ea79def --- /dev/null +++ b/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"" + + +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"" diff --git a/frest/auth/routes.py b/frest/auth/routes.py new file mode 100644 index 0000000..27f00ec --- /dev/null +++ b/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("Authorization") + 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/", 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/") +@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/", 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/", 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/frest/database.py b/frest/database.py new file mode 100644 index 0000000..176cd52 --- /dev/null +++ b/frest/database.py @@ -0,0 +1,4 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() +config = {"DATABASE_URI": "sqlite:///database.db"} diff --git a/frest/decorators.py b/frest/decorators.py new file mode 100644 index 0000000..7ce79d7 --- /dev/null +++ b/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("Authorization"): + abort(403) + + auth = request.headers.get("Authorization") + 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("Authorization") + 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/frest/forms.py b/frest/forms.py new file mode 100644 index 0000000..8ce2e95 --- /dev/null +++ b/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/frest/mail.py b/frest/mail.py new file mode 100644 index 0000000..e553525 --- /dev/null +++ b/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/frest/manage/__init__.py b/frest/manage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frest/manage/bcolors.py b/frest/manage/bcolors.py new file mode 100644 index 0000000..eb8b803 --- /dev/null +++ b/frest/manage/bcolors.py @@ -0,0 +1,17 @@ +class bcolors(object): + DARK_GREY = "\033[90m" + BOLD = "\033[1m" + ERROR = "\033[91m" + OK = "\033[92m" + WARNING = "\033[93m" + ENDC = "\033[0m" + + +COLORS = [ + bcolors.DARK_GREY, + bcolors.BOLD, + bcolors.ERROR, + bcolors.OK, + bcolors.WARNING, + bcolors.ENDC, +] diff --git a/frest/manage/utils.py b/frest/manage/utils.py new file mode 100644 index 0000000..6634e05 --- /dev/null +++ b/frest/manage/utils.py @@ -0,0 +1,228 @@ +import os +from .bcolors import COLORS +import re + + +ENDC = len(COLORS) - 1 +TEMPLATE_PATH = os.path.join(frest.__path__[0], "templates") + + +def logging(text, _type=ENDC, end=""): + print(f"{COLORS[_type]}{text}{COLORS[ENDC]}", end=end) + + +def logging_arg(text, *args): + args = [f"{COLORS[0]}{i}{COLORS[ENDC]}" for i in args] + print(text.format(*args), end="") + + +def logo(): + print( + """ + __ _ + / _| | | + | |_ _ __ ___ ___| |_ + | _| '__/ _ \/ __| __| + | | | | | __/\__ \ |_ + |_| |_| \___||___/\__| + \n\n""" + ) + + +def inputsr(): + return input().strip().replace(" ", "_").lower() + + +def create_field(fields): + field = {"name": "", "type": "", "nullable": True} + logging("Choose field name: ") + field_name = inputsr() + while (len(field_name) < 1 or field_name[0].isdigit()) or ( + field_name in fields or field_name in ["id", "created_at", "updated_at"] + ): + if len(field_name) < 1 or field_name[0].isdigit(): + logging("Field name must not be empty or starts with a number", 2, "\n") + else: + logging("Field name already exists", 2, "\n") + + logging("Choose field name: ") + field_name = inputsr() + + field["name"] = field_name + + logging("Choose field type: ") + logging("int, str, text, datetime, float, bool", 0, " ") + field_type = inputsr() + if field_type not in ["int", "str", "text", "datetime", "float", "bool"]: + logging("Field type must be one of the supported type", 2, "\n") + logging("Choose field type: ") + logging("int, str, text, datetime, float, bool", 0, " ") + field_type = inputsr() + + if field_type == "str": + logging("Choose string size: ") + stringsize = inputsr() + while not stringsize.isdigit(): + logging("String size must be an integer number", 2, "\n") + logging("Choose string size: ") + stringsize = inputsr() + + stringsize = int(stringsize) + if stringsize < 1: + logging("You inserted 0, so it will be 1 by default", 4, "\n") + stringsize = 1 + + field["size"] = stringsize + + field["type"] = field_type + + logging("Field is nullable? (Y/n): ") + field_nullable = inputsr() + + if len(field_nullable) > 0: + while True: + if field_nullable[0] not in ["y", "n"]: + logging("Field is nullable? (Y/n): ") + field_nullable = inputsr() + else: + break + + if field_nullable[0] == "n": + field["nullable"] = False + + return field + + +def create_model_cli(name): + logging( + "Fields id, created_at, updated_at are default on every new model, you can delete it from model file", + 4, + "\n\n", + ) + fields = [] + logging("Create field? (Y/n): ") + answer = inputsr() + + while answer in ["y", "", "yes"]: + field = create_field(fields) + fields.append(field) + + logging("Create new field? (Y/n): ") + answer = inputsr() + + fields_string = "" + init_fields_s = "" + for field in fields: + field_string = f"{field['name']} = db.Column(" + if field["type"] == "int": + field_string += "db.Integer" + elif field["type"] == "str": + field_string += f"db.String({field['size']})" + elif field["type"] == "text": + field_string += "db.Text" + elif field["type"] == "datetime": + field_string += "db.Datetime" + elif field["type"] == "float": + field_string += "db.Float" + else: + field_string += "db.Boolean" + + field_string += f", nullable={field['nullable']})\n\t".replace("\t", " " * 4) + + fields_string += field_string + init_fields_s += f"self.{field['name']} = kwargs.get('{field['name']}')\n\t\t".replace( + "'", '"' + ).replace( + "\t", " " * 4 + ) + + with open(os.path.join(TEMPLATE_PATH, "models.txt")) as f: + modeltext = "".join(f.readlines()) + + modeltext = modeltext.replace("%%NAME%%", name.capitalize()) + modeltext = modeltext.replace("%%name%%", name) + modeltext = modeltext.replace("%%params_model%%", fields_string[:-5]) + modeltext = modeltext.replace("%%params_model_init%%", init_fields_s[:-9]) + + modelpath = f"scheme/{name}/models.py" + logging_arg("Create {}... ", modelpath) + with open(modelpath, "w") as f: + f.write(modeltext) + + return fields + + +def create_forms(name): + with open(os.path.join(TEMPLATE_PATH, "form.txt")) as f: + formstext = "".join(f.readlines()) + + formstext = formstext.replace("%%NAME%%", name.capitalize()) + with open(f"scheme/{name}/forms.py", "w") as f: + f.write(formstext) + + +def create_routes(name, fields): + fields_l = [f"{name}Id"] + fields_l.extend([i["name"] for i in fields]) + fields_l.extend(["created_at", "updated_at"]) + + params_form = "" + for i in fields: + params_form += f"{i['name']}=form.get('{i['name']}'),\n\t\t\t".replace( + "'", '"' + ).replace("\t", " " * 4) + + params_put = "" + for i in fields: + params_put += f"{name[0]}.{i['name']} = form.get('{i['name']}')\n\t\t".replace( + "'", '"' + ).replace("\t", " " * 4) + + with open(os.path.join(TEMPLATE_PATH, "routes.txt")) as f: + routestext = "".join(f.readlines()) + + routestext = routestext.replace("%%NAME%%", name.capitalize()) + routestext = routestext.replace("%%name%%", name) + routestext = routestext.replace("%%params%%", ",".join(fields_l)) + routestext = routestext.replace("%%first_char%%", name[0]) + routestext = routestext.replace("%%params_form%%", params_form[:-13]) + routestext = routestext.replace("%%params_put%%", params_put[:-9]) + + with open(f"scheme/{name}/routes.py", "w") as f: + f.write(routestext) + + +def create_app(name): + name = name.lower().replace("-", "_") + + if name.isdigit() or name[0].isdigit(): + logging("Name cannot be a number o starts with a number", 2) + return + + if len(name) < 2: + logging("Name of app must be minimun 2 characters long", 2) + return + + if os.path.exists(f"scheme/{name}"): + logging("App already exists", 2) + return + + logging_arg("Create {}... ", f"scheme/{name}") + os.mkdir(f"scheme/{name}") + logging("OK", 3, "\n") + + logging_arg("Create {}... ", f"scheme/{name}/__init__.py") + open(f"scheme/{name}/__init__.py", "w").close() + logging("OK", 3, "\n") + + logging_arg("Create model for {}...\n", name) + fields = create_model_cli(name) + logging("OK", 3, "\n") + + logging_arg("Create {}... ", f"scheme/{name}/forms.py") + create_forms(name) + logging("OK", 3, "\n") + + logging_arg("Create {}... ", f"scheme/{name}/routes.py") + create_routes(name, fields) + logging("OK", 3, "\n") diff --git a/frest/templates/form.txt b/frest/templates/form.txt new file mode 100644 index 0000000..ed1017e --- /dev/null +++ b/frest/templates/form.txt @@ -0,0 +1,11 @@ +from .models import %%NAME%% +from src.forms import ModelForm + + +class %%NAME%%Form(ModelForm): + model = %%NAME%% + + def __init__(self, data): + super().__init__(self.model) + self.data = data + self.ignore = [] diff --git a/frest/templates/models.txt b/frest/templates/models.txt new file mode 100644 index 0000000..af80963 --- /dev/null +++ b/frest/templates/models.txt @@ -0,0 +1,26 @@ +from database import db +from datetime import datetime +from pytz import timezone +import os + + +class %%NAME%%(db.Model): + %%name%%Id = db.Column(db.Integer, primary_key=True) + %%params_model%% + created_at = db.Column(db.DateTime) + updated_at = db.Column(db.DateTime) + + def __init__(self, **kwargs): + %%params_model_init%% + self.created_at = datetime.now( + timezone(os.getenv("FREST_TIMEZONE", "Europe/Rome")) + ) + self.updated_at = datetime.now( + timezone(os.getenv("FREST_TIMEZONE", "Europe/Rome")) + ) + + def __repr__(self): + return f"<%%NAME%% '{self.%%name%%Id}'>" + + def __str__(self): + return f"{self.%%name%%Id}" diff --git a/frest/templates/routes.txt b/frest/templates/routes.txt new file mode 100644 index 0000000..11ba63a --- /dev/null +++ b/frest/templates/routes.txt @@ -0,0 +1,88 @@ +from flask import Blueprint, request, abort +from frest.utils import http_call, model_serialize +from frest.decorators import check_token, admin_required +from .models import %%NAME%% +from .forms import %%NAME%%Form +from database import db +import json +from datetime import datetime +from pytz import timezone + +api = Blueprint("%%name%%s", __name__) + + +@api.route("/api/%%name%%") +def all_%%name%%s(): + return http_call( + [ + model_serialize(i, params="%%params%%") + for i in %%NAME%%.query.all() + ], + 200, + ) + + +@api.route("/api/%%name%%/<%%name%%Id>") +def get_%%name%%(%%name%%Id): + %%first_char%% = %%NAME%%.query.filter_by(%%name%%Id=%%name%%Id).first() + if not %%first_char%%: + abort(404) + + return http_call(model_serialize(%%first_char%%, params="%%params%%"), 200) + + +@api.route("/api/%%name%%/<%%name%%Id>", methods=["DELETE"]) +@check_token +def delete_%%name%%(%%name%%Id): + %%first_char%% = %%NAME%%.query.filter_by(%%name%%Id=%%name%%Id) + if not %%first_char%%: + abort(404) + + deleted = %%first_char%%.delete() + db.session.commit() + + return http_call({"delete": deleted}, 200) + + +@api.route("/api/%%name%%", methods=["POST"]) +@check_token +def new_%%name%%(): + if not request.json: + abort(400) + + form = %%NAME%%Form(request.json) + + if form.is_valid(): + %%first_char%% = %%NAME%%( + %%params_form%% + ) + db.session.add(%%first_char%%) + + db.session.commit() + + return http_call({"%%name%%Id": %%first_char%%.%%name%%Id}, 201) + + abort(400) + + +@api.route("/api/%%name%%/<%%name%%Id>", methods=["PUT"]) +@check_token +def edit_%%name%%(%%name%%Id): + if not request.json: + abort(400) + + form = %%NAME%%Form(request.json) + + if form.is_valid(): + %%first_char%% = %%NAME%%.query.filter_by(%%name%%Id=%%name%%Id).first() + if not %%first_char%%: + abort(404) + + %%params_put%% + %%first_char%%.updated_at = datetime.now(timezone("Europe/Rome")) + + db.session.commit() + + return http_call({"%%name%%Id": %%first_char%%.%%name%%Id}, 200) + + abort(400) diff --git a/frest/utils.py b/frest/utils.py new file mode 100644 index 0000000..ef673a7 --- /dev/null +++ b/frest/utils.py @@ -0,0 +1,44 @@ +from flask import make_response, jsonify, request, render_template +from flask_mail import Message +from frest.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 diff --git a/frest/wsgi.py b/frest/wsgi.py new file mode 100644 index 0000000..6026b0f --- /dev/null +++ b/frest/wsgi.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == "__main__": + app.run() diff --git a/src/frest/auth/__init__.py b/src/frest/auth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/frest/auth/forms.py b/src/frest/auth/forms.py deleted file mode 100644 index abc2f49..0000000 --- a/src/frest/auth/forms.py +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index ea79def..0000000 --- a/src/frest/auth/models.py +++ /dev/null @@ -1,52 +0,0 @@ -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"" - - -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"" diff --git a/src/frest/auth/routes.py b/src/frest/auth/routes.py deleted file mode 100644 index 66bd8f5..0000000 --- a/src/frest/auth/routes.py +++ /dev/null @@ -1,183 +0,0 @@ -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("Authorization") - 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/", 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/") -@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/", 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/", 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 deleted file mode 100644 index 176cd52..0000000 --- a/src/frest/database.py +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 7ce79d7..0000000 --- a/src/frest/decorators.py +++ /dev/null @@ -1,40 +0,0 @@ -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("Authorization"): - abort(403) - - auth = request.headers.get("Authorization") - 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("Authorization") - 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 deleted file mode 100644 index 8ce2e95..0000000 --- a/src/frest/forms.py +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index e553525..0000000 --- a/src/frest/mail.py +++ /dev/null @@ -1,11 +0,0 @@ -from flask_mail import Mail - -mail = Mail() -config = { - "SERVER": "", - "PORT": 587, - "USE_TLS": True, - "USERNAME": "", - "DEFAULT_SENDER": "", - "PASSWORD": "", -} diff --git a/src/frest/manage.py b/src/frest/manage.py deleted file mode 100644 index 1164d70..0000000 --- a/src/frest/manage.py +++ /dev/null @@ -1,23 +0,0 @@ -import argparse -from manage.utils import logo, create_app -from manage.utils import logging, logging_arg -import os - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--startapp", "-s", type=str, help="create new app") - args = parser.parse_args() - - if args.startapp: - if not os.path.exists("scheme"): - logging_arg("Create {}... ", "scheme/") - logging("OK", 3, "\n") - os.mkdir("scheme") - - create_app(args.startapp) - - -if __name__ == "__main__": - logo() - main() diff --git a/src/frest/manage/__init__.py b/src/frest/manage/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/frest/manage/bcolors.py b/src/frest/manage/bcolors.py deleted file mode 100644 index eb8b803..0000000 --- a/src/frest/manage/bcolors.py +++ /dev/null @@ -1,17 +0,0 @@ -class bcolors(object): - DARK_GREY = "\033[90m" - BOLD = "\033[1m" - ERROR = "\033[91m" - OK = "\033[92m" - WARNING = "\033[93m" - ENDC = "\033[0m" - - -COLORS = [ - bcolors.DARK_GREY, - bcolors.BOLD, - bcolors.ERROR, - bcolors.OK, - bcolors.WARNING, - bcolors.ENDC, -] diff --git a/src/frest/manage/utils.py b/src/frest/manage/utils.py deleted file mode 100644 index f8e76d1..0000000 --- a/src/frest/manage/utils.py +++ /dev/null @@ -1,226 +0,0 @@ -import os -from .bcolors import COLORS - - -ENDC = len(COLORS) - 1 - - -def logging(text, _type=ENDC, end=""): - print(f"{COLORS[_type]}{text}{COLORS[ENDC]}", end=end) - - -def logging_arg(text, *args): - args = [f"{COLORS[0]}{i}{COLORS[ENDC]}" for i in args] - print(text.format(*args), end="") - - -def logo(): - print( - """ - __ _ - / _| | | - | |_ _ __ ___ ___| |_ - | _| '__/ _ \/ __| __| - | | | | | __/\__ \ |_ - |_| |_| \___||___/\__| - \n\n""" - ) - - -def inputsr(): - return input().strip().replace(" ", "_").lower() - - -def create_field(fields): - field = {"name": "", "type": "", "nullable": True} - logging("Choose field name: ") - field_name = inputsr() - while (len(field_name) < 1 or field_name[0].isdigit()) or ( - field_name in fields or field_name in ["id", "created_at", "updated_at"] - ): - if len(field_name) < 1 or field_name[0].isdigit(): - logging("Field name must not be empty or starts with a number", 2, "\n") - else: - logging("Field name already exists", 2, "\n") - - logging("Choose field name: ") - field_name = inputsr() - - field["name"] = field_name - - logging("Choose field type: ") - logging("int, str, text, datetime, float, bool", 0, " ") - field_type = inputsr() - if field_type not in ["int", "str", "text", "datetime", "float", "bool"]: - logging("Field type must be one of the supported type", 2, "\n") - logging("Choose field type: ") - logging("int, str, text, datetime, float, bool", 0, " ") - field_type = inputsr() - - if field_type == "str": - logging("Choose string size: ") - stringsize = inputsr() - while not stringsize.isdigit(): - logging("String size must be an integer number", 2, "\n") - logging("Choose string size: ") - stringsize = inputsr() - - stringsize = int(stringsize) - if stringsize < 1: - logging("You inserted 0, so it will be 1 by default", 4, "\n") - stringsize = 1 - - field["size"] = stringsize - - field["type"] = field_type - - logging("Field is nullable? (Y/n): ") - field_nullable = inputsr() - - if len(field_nullable) > 0: - while True: - if field_nullable[0] not in ["y", "n"]: - logging("Field is nullable? (Y/n): ") - field_nullable = inputsr() - else: - break - - if field_nullable[0] == "n": - field["nullable"] = False - - return field - - -def create_model_cli(name): - logging( - "Fields id, created_at, updated_at are default on every new model, you can delete it from model file", - 4, - "\n\n", - ) - fields = [] - logging("Create field? (Y/n): ") - answer = inputsr() - - while answer in ["y", "", "yes"]: - field = create_field(fields) - fields.append(field) - - logging("Create new field? (Y/n): ") - answer = inputsr() - - fields_string = "" - init_fields_s = "" - for field in fields: - field_string = f"{field['name']} = db.Column(" - if field["type"] == "int": - field_string += "db.Integer" - elif field["type"] == "str": - field_string += f"db.String({field['size']})" - elif field["type"] == "text": - field_string += "db.Text" - elif field["type"] == "datetime": - field_string += "db.Datetime" - elif field["type"] == "float": - field_string += "db.Float" - else: - field_string += "db.Boolean" - - field_string += f", nullable={field['nullable']})\n\t".replace("\t", " " * 4) - - fields_string += field_string - init_fields_s += f"self.{field['name']} = kwargs.get('{field['name']}')\n\t\t".replace( - "'", '"' - ).replace( - "\t", " " * 4 - ) - - with open("templates/models.txt") as f: - modeltext = "".join(f.readlines()) - - modeltext = modeltext.replace("%%NAME%%", name.capitalize()) - modeltext = modeltext.replace("%%name%%", name) - modeltext = modeltext.replace("%%params_model%%", fields_string[:-5]) - modeltext = modeltext.replace("%%params_model_init%%", init_fields_s[:-9]) - - modelpath = f"scheme/{name}/models.py" - logging_arg("Create {}... ", modelpath) - with open(modelpath, "w") as f: - f.write(modeltext) - - return fields - - -def create_forms(name): - with open("templates/form.txt") as f: - formstext = "".join(f.readlines()) - - formstext = formstext.replace("%%NAME%%", name.capitalize()) - with open(f"scheme/{name}/forms.py", "w") as f: - f.write(formstext) - - -def create_routes(name, fields): - fields_l = [f"{name}Id"] - fields_l.extend([i["name"] for i in fields]) - fields_l.extend(["created_at", "updated_at"]) - - params_form = "" - for i in fields: - params_form += f"{i['name']}=form.get('{i['name']}'),\n\t\t\t".replace( - "'", '"' - ).replace("\t", " " * 4) - - params_put = "" - for i in fields: - params_put += f"{name[0]}.{i['name']} = form.get('{i['name']}')\n\t\t".replace( - "'", '"' - ).replace("\t", " " * 4) - - with open("templates/routes.txt") as f: - routestext = "".join(f.readlines()) - - routestext = routestext.replace("%%NAME%%", name.capitalize()) - routestext = routestext.replace("%%name%%", name) - routestext = routestext.replace("%%params%%", ",".join(fields_l)) - routestext = routestext.replace("%%first_char%%", name[0]) - routestext = routestext.replace("%%params_form%%", params_form[:-13]) - routestext = routestext.replace("%%params_put%%", params_put[:-9]) - - with open(f"scheme/{name}/routes.py", "w") as f: - f.write(routestext) - - -def create_app(name): - name = name.lower().replace("-", "_") - - if name.isdigit() or name[0].isdigit(): - logging("Name cannot be a number o starts with a number", 2) - return - - if len(name) < 2: - logging("Name of app must be minimun 2 characters long", 2) - return - - if os.path.exists(f"scheme/{name}"): - logging("App already exists", 2) - return - - logging_arg("Create {}... ", f"scheme/{name}") - os.mkdir(f"scheme/{name}") - logging("OK", 3, "\n") - - logging_arg("Create {}... ", f"scheme/{name}/__init__.py") - open(f"scheme/{name}/__init__.py", "w").close() - logging("OK", 3, "\n") - - logging_arg("Create model for {}...\n", name) - fields = create_model_cli(name) - logging("OK", 3, "\n") - - logging_arg("Create {}... ", f"scheme/{name}/forms.py") - create_forms(name) - logging("OK", 3, "\n") - - logging_arg("Create {}... ", f"scheme/{name}/routes.py") - create_routes(name, fields) - logging("OK", 3, "\n") diff --git a/src/frest/templates/form.txt b/src/frest/templates/form.txt deleted file mode 100644 index ed1017e..0000000 --- a/src/frest/templates/form.txt +++ /dev/null @@ -1,11 +0,0 @@ -from .models import %%NAME%% -from src.forms import ModelForm - - -class %%NAME%%Form(ModelForm): - model = %%NAME%% - - def __init__(self, data): - super().__init__(self.model) - self.data = data - self.ignore = [] diff --git a/src/frest/templates/models.txt b/src/frest/templates/models.txt deleted file mode 100644 index af80963..0000000 --- a/src/frest/templates/models.txt +++ /dev/null @@ -1,26 +0,0 @@ -from database import db -from datetime import datetime -from pytz import timezone -import os - - -class %%NAME%%(db.Model): - %%name%%Id = db.Column(db.Integer, primary_key=True) - %%params_model%% - created_at = db.Column(db.DateTime) - updated_at = db.Column(db.DateTime) - - def __init__(self, **kwargs): - %%params_model_init%% - self.created_at = datetime.now( - timezone(os.getenv("FREST_TIMEZONE", "Europe/Rome")) - ) - self.updated_at = datetime.now( - timezone(os.getenv("FREST_TIMEZONE", "Europe/Rome")) - ) - - def __repr__(self): - return f"<%%NAME%% '{self.%%name%%Id}'>" - - def __str__(self): - return f"{self.%%name%%Id}" diff --git a/src/frest/templates/routes.txt b/src/frest/templates/routes.txt deleted file mode 100644 index 11ba63a..0000000 --- a/src/frest/templates/routes.txt +++ /dev/null @@ -1,88 +0,0 @@ -from flask import Blueprint, request, abort -from frest.utils import http_call, model_serialize -from frest.decorators import check_token, admin_required -from .models import %%NAME%% -from .forms import %%NAME%%Form -from database import db -import json -from datetime import datetime -from pytz import timezone - -api = Blueprint("%%name%%s", __name__) - - -@api.route("/api/%%name%%") -def all_%%name%%s(): - return http_call( - [ - model_serialize(i, params="%%params%%") - for i in %%NAME%%.query.all() - ], - 200, - ) - - -@api.route("/api/%%name%%/<%%name%%Id>") -def get_%%name%%(%%name%%Id): - %%first_char%% = %%NAME%%.query.filter_by(%%name%%Id=%%name%%Id).first() - if not %%first_char%%: - abort(404) - - return http_call(model_serialize(%%first_char%%, params="%%params%%"), 200) - - -@api.route("/api/%%name%%/<%%name%%Id>", methods=["DELETE"]) -@check_token -def delete_%%name%%(%%name%%Id): - %%first_char%% = %%NAME%%.query.filter_by(%%name%%Id=%%name%%Id) - if not %%first_char%%: - abort(404) - - deleted = %%first_char%%.delete() - db.session.commit() - - return http_call({"delete": deleted}, 200) - - -@api.route("/api/%%name%%", methods=["POST"]) -@check_token -def new_%%name%%(): - if not request.json: - abort(400) - - form = %%NAME%%Form(request.json) - - if form.is_valid(): - %%first_char%% = %%NAME%%( - %%params_form%% - ) - db.session.add(%%first_char%%) - - db.session.commit() - - return http_call({"%%name%%Id": %%first_char%%.%%name%%Id}, 201) - - abort(400) - - -@api.route("/api/%%name%%/<%%name%%Id>", methods=["PUT"]) -@check_token -def edit_%%name%%(%%name%%Id): - if not request.json: - abort(400) - - form = %%NAME%%Form(request.json) - - if form.is_valid(): - %%first_char%% = %%NAME%%.query.filter_by(%%name%%Id=%%name%%Id).first() - if not %%first_char%%: - abort(404) - - %%params_put%% - %%first_char%%.updated_at = datetime.now(timezone("Europe/Rome")) - - db.session.commit() - - return http_call({"%%name%%Id": %%first_char%%.%%name%%Id}, 200) - - abort(400) diff --git a/src/frest/utils.py b/src/frest/utils.py deleted file mode 100644 index 934aec4..0000000 --- a/src/frest/utils.py +++ /dev/null @@ -1,44 +0,0 @@ -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 -- cgit v1.2.3-18-g5258