diff --git a/api.py b/api.py new file mode 100644 index 0000000..57f1a80 --- /dev/null +++ b/api.py @@ -0,0 +1,99 @@ +from flask import Blueprint, render_template, redirect, url_for, request +from flask_login import login_required, current_user +from models import db +from models import User, Item +import json +from sqlalchemy import desc, text + +api_bp = Blueprint("api", __name__, url_prefix="/api") + +@api_bp.route("/validate_key", methods=["POST"]) +def validate_key(): + result={} + result['status']=0 + result['data'] = '' + if "key" in request.form: + key = request.form["key"] + print(key) + if key == 'NogNietNodigHier': + result['status'] = 1 + else: + result['message'] = 'Unknown Application-Key' + return json.dumps(result), 200 + +@api_bp.route("/login", methods=["POST"]) +def login(): + result={} + result['status']=0 + result['data'] = '' + if "user" in request.form and "password" in request.form: + user = request.form["user"] + password = request.form["password"] + print(user) + user = User.query.filter_by(name=user).first() + if user and user.check_password(password): + if not user.is_approved: + result['message'] = 'User is pending approval.' + else: + result['status'] = 1 + result['data'] = user.as_dict() + else: + result['message'] = 'User or password unknown' + return json.dumps(result), 200 + +@api_bp.route("/register", methods=["POST"]) +def register(): + result={} + result['status']=0 + result['data'] = '' + if "user" in request.form and "password" in request.form: + username = request.form["user"] + password = request.form["password"] + print(username) + user = User.query.filter_by(name=username).first() + if user: + result['message'] = 'Username is not unique. Pick something else please.' + else: + result['status'] = 2 + result['message'] = 'Registration Ok. Awaiting approval.' + user = User(name=username) + user.set_password(password) + db.session.add(user) + db.session.commit() + + return json.dumps(result), 200 + +@api_bp.route("/load_lists", methods=["POST"]) +def load_lists(): + print("Trying to load lists") + result={} + result['status']=1 # from this call the result is always 1, empty or not + result['data'] = '' + if "userid" in request.form: + userid = int(request.form["userid"]) + + print(userid) + user = User.query.filter_by(id=userid).first() + if user: + result['data'] = user.all_my_lists_as_dict() + + return json.dumps(result), 200 + +@api_bp.route("/load_items", methods=["POST"]) +def load_items(): + print("Trying to load items") + result={} + result['status']=1 # from this call the result is always 1, empty or not + result['data'] = '' + if "listid" in request.form: + listid = int(request.form["listid"]) + + print(listid) + items=Item.query.filter_by(listofitems_id=listid, is_suggestion=False).order_by(text("is_checked, category, label")).all() + print(items) + if items: + result['data'] = [ i.as_dict() for i in items] + + print(result) + return json.dumps(result), 200 + diff --git a/auth.py b/auth.py index 25f7cf4..d563f47 100644 --- a/auth.py +++ b/auth.py @@ -1,14 +1,82 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response from flask_login import login_user, logout_user, login_required, current_user from models import db from models import User from log import Log +import pickle +from cryptography.fernet import Fernet +import datetime + auth_bp = Blueprint("auth", __name__) +# ===================================================== +# SECRET KEY (only your program has this) +# ===================================================== +# Generate once using: Fernet.generate_key() +_SECRET_KEY = b'YFK7QCyTzhyLO4vqrnRxvDAI5uu8mXEYrInEjbRoQgs=' + +fernet = Fernet(_SECRET_KEY) + +# ===================================================== +# ENCRYPT +# ===================================================== +def encrypt_object(obj) -> bytes: + """ + Encrypt any Python object and return encrypted bytes. + """ + serialized = pickle.dumps(obj) + encrypted = fernet.encrypt(serialized) + return encrypted + +# ===================================================== +# DECRYPT +# ===================================================== +def decrypt_object(encrypted_data: bytes): + """ + Decrypt bytes back into the original Python object. + """ + decrypted = fernet.decrypt(encrypted_data) + obj = pickle.loads(decrypted) + return obj + +def create_cookie(userid): + cookie_data = { + "user": userid, + "datetime": datetime.datetime.now() + } + return encrypt_object(cookie_data).decode('utf-8') + +def is_cookie_ok_to_autologin(cookie_content): + user = 0 + autologin = False + try: + cookie_data = decrypt_object(cookie_content) + user = cookie_data["user"] + dt = cookie_data["datetime"] + lastweek = datetime.datetime.now() - datetime.timedelta(days=7) + autologin = dt > lastweek + except: + Log.info("Cookie error") + if autologin: + return user + return 0 + + + + #. @auth_bp.route("/", methods=["GET", "POST"]) @auth_bp.route("/login", methods=["GET", "POST"]) def login(): + username = request.cookies.get('username') + + userid = is_cookie_ok_to_autologin(username) + if userid>0: + user = User.query.filter_by(id=userid).first() + if user and user.is_approved: + login_user(user) + return redirect(url_for("lists.home")) + if request.method == "POST": user = User.query.filter_by(name=request.form["name"]).first() if user and user.check_password(request.form["password"]): @@ -18,7 +86,12 @@ def login(): return redirect(url_for("lists.home")) flash("Invalid credentials") Log.info(f"Authorization failure for user '{request.form["name"]}' ") - return render_template("login.html") + + + resp = make_response(render_template("login.html")) + resp.set_cookie('username', '') + return resp + @auth_bp.route("/register", methods=["GET", "POST"]) def register(): @@ -34,5 +107,27 @@ def register(): @login_required def logout(): logout_user() - return redirect(url_for("auth.login")) + resp = make_response(redirect(url_for("auth.login"))) + resp.set_cookie('username', '') + return resp + +@auth_bp.route("/change_pwd", methods=["GET", "POST"]) +def change_pwd(): + if request.method == "POST": + pwd_old = request.form["password_old"] + pwd_new1 = request.form["password_new1"] + pwd_new2 = request.form["password_new2"] + print(f" >>{pwd_old} {pwd_new1} {pwd_new2}") + print(f"cp: {current_user.name}") + if current_user.check_password(pwd_old): + if pwd_new1 == pwd_new2: + current_user.set_password(pwd_new2) + db.session.commit() + return redirect(url_for("lists.home")) + flash("New Passwords are not identical") + flash("Invalid credentials") + Log.info(f"Authorization failure for user '{request.form["name"]}' ") + + return render_template(url_for("lists.home")) + return render_template("change_pwd.html") \ No newline at end of file diff --git a/items.py b/items.py index cf414c5..96d8c8b 100644 --- a/items.py +++ b/items.py @@ -1,10 +1,11 @@ -from flask import Blueprint, render_template, redirect, url_for, request +from flask import Blueprint, render_template, redirect, url_for, request, make_response from flask_login import login_required, current_user from models import db from models import Item from sqlalchemy import desc, text import os from log import Log +from auth import create_cookie items_bp = Blueprint("items", __name__) @@ -14,12 +15,14 @@ ICONPATH = "static/categories/" #without starting / and with ending / @items_bp.route("/items/") @login_required def items(listid): - return render_template("items_show.html", user=current_user, + resp = make_response(render_template("items_show.html", user=current_user, items=Item.query.filter_by(listofitems_id=listid, is_suggestion=False).order_by(text("is_checked, category, label")).all(), button_top_url1 = url_for("items.items_append", listid=listid), button_top_txt1 = "add", button_top_url2 = url_for("items.items_clean", listid=listid), - button_top_txt2 = "clean") + button_top_txt2 = "clean")) + resp.set_cookie('username', create_cookie(current_user.id)) + return resp # webservice for updating checked state of one item @items_bp.route("/item_update//") @@ -124,12 +127,14 @@ def items_append(listid): if not i.startswith('.'): icon_names.append(os.path.splitext(i)[0]) - return render_template("items_append.html", user=current_user, + resp = make_response(render_template("items_append.html", user=current_user, listid=listid, items=Item.query.filter_by(listofitems_id=listid).order_by(Item.label).all(), icons = icon_names, iconpath = '/'+ICONPATH, - logo_url=url_for("items.items", listid=listid)) + logo_url=url_for("items.items", listid=listid))) + resp.set_cookie('username', create_cookie(current_user.id)) + return resp # user adds items to list @items_bp.route("/items_multiappend/", methods=["GET", "POST"]) @@ -188,9 +193,12 @@ def item_addone(itemid): if not i.unit: i.unit = 'x' db.session.commit() - return render_template("items_append.html", user=current_user, + + resp = make_response(render_template("items_append.html", user=current_user, items=Item.query.filter_by(listofitems_id=listid).order_by(Item.label).all(), - logo_url=url_for("items.items", listid=listid)) + logo_url=url_for("items.items", listid=listid))) + resp.set_cookie('username', create_cookie(current_user.id)) + return resp # webservice for adding one item with a quantity/unit @items_bp.route("/item_addquantity//") @@ -206,9 +214,12 @@ def item_addquantity(itemid, quantity): else: i.quantity += int(quantity) db.session.commit() - return render_template("items_append.html", user=current_user, + + resp = make_response(render_template("items_append.html", user=current_user, items=Item.query.filter_by(listofitems_id=listid).order_by(Item.label).all(), - logo_url=url_for("items.items", listid=listid)) + logo_url=url_for("items.items", listid=listid))) + resp.set_cookie('username', create_cookie(current_user.id)) + return resp # webservice for updating category of one item @items_bp.route("/item_update_category//") diff --git a/lapp.py b/lapp.py index 4be71aa..9f5a9ed 100644 --- a/lapp.py +++ b/lapp.py @@ -6,6 +6,7 @@ from auth import auth_bp from admin import admin_bp from lists import lists_bp from items import items_bp +from api import api_bp login_manager = LoginManager() @@ -22,6 +23,7 @@ def create_app(): app.register_blueprint(admin_bp) app.register_blueprint(lists_bp) app.register_blueprint(items_bp) + app.register_blueprint(api_bp) with app.app_context(): db.create_all() @@ -36,4 +38,4 @@ def create_app(): if __name__ == "__main__": my_app = create_app() # if needed - initialize data - my_app.run(debug=True, host='0.0.0.0', port=5001) + my_app.run(debug=True, host='127.0.0.1', port=5001) diff --git a/lists.py b/lists.py index 57ad7d4..241af60 100644 --- a/lists.py +++ b/lists.py @@ -1,8 +1,9 @@ -from flask import Blueprint, render_template, redirect, url_for, request +from flask import Blueprint, render_template, redirect, url_for, request, make_response from flask_login import login_required, current_user from models import db from models import User, ListOfItems, Item, Shared from log import Log +from auth import create_cookie lists_bp = Blueprint("lists", __name__) @@ -17,7 +18,11 @@ def not_current_user(current_user_id): @lists_bp.route("/") @login_required def home(): - return render_template("lists_show.html", user=current_user, ilists=current_user.all_my_lists()) + resp = make_response(render_template("lists_show.html", user=current_user, ilists=current_user.all_my_lists(), + button_top_url1 = url_for("auth.change_pwd"), + button_top_txt1 = "add",)) + resp.set_cookie('username', create_cookie(current_user.id)) + return resp @lists_bp.route("/lists_edit", methods=["GET", "POST"]) @login_required @@ -77,7 +82,9 @@ def edit(): return home() - return render_template("lists_edit.html", user=current_user, + resp = make_response(render_template("lists_edit.html", user=current_user, ilists=current_user.my_owned_lists(with_inactive=True), users=not_current_user(current_user.id), - newlist=newlist) + newlist=newlist)) + resp.set_cookie('username', create_cookie(current_user.id)) + return resp diff --git a/models.py b/models.py index 0ca8c67..f48f0e5 100644 --- a/models.py +++ b/models.py @@ -15,7 +15,14 @@ class User(UserMixin, db.Model): is_private = db.Column(db.Boolean, default=False) is_guest = db.Column(db.Boolean, default=False) + def as_dict(self): + result = {} + for p in ['id','name','is_admin','is_approved','is_private','is_guest']: + result[p] = getattr(self, p) + return result + def set_password(self, password): + print(f"u:{self.name} > {password}") self.password_hash = generate_password_hash(password) def check_password(self, password): @@ -34,11 +41,18 @@ class User(UserMixin, db.Model): result.append(l) return result + def all_my_lists_as_dict(self): + result = [] + for l in self.all_my_lists(): + result.append(l.as_dict()) + return result + def shares_in_list(self, listofitem_id): shared = Shared.query.filter_by(user_id=self.id, listofitem_id=listofitem_id).first() return True if shared else False + class ListOfItems(db.Model): id = db.Column(db.Integer, primary_key=True) updated_at = db.Column(db.TIMESTAMP, default=datetime.now(), onupdate=datetime.now(), nullable=False) @@ -46,6 +60,15 @@ class ListOfItems(db.Model): name = db.Column(db.String(40), unique=True, nullable=False) is_active = db.Column(db.Boolean, default=False) + def as_dict(self): + result = {} + for p in ['id','name','owner_user_id','is_active']: + result[p] = getattr(self, p) + result["pending"] = self.items_left() + result["total"] = self.items_total() + print(result) + return result + def items_left(self): return len(Item.query.filter_by(listofitems_id=self.id, is_checked=False, is_suggestion=False).all()) @@ -65,6 +88,12 @@ class Item(db.Model): is_suggestion = db.Column(db.Boolean, default=False) category = db.Column(db.String(20)) + def as_dict(self): + result = {} + for p in ['id','listofitems_id','label','quantity','unit','is_checked', 'is_suggestion', 'category']: + result[p] = getattr(self, p) + return result + class Shared(db.Model): user_id = db.Column(db.Integer, primary_key=True) listofitem_id = db.Column(db.Integer, primary_key=True) diff --git a/static/password.svg b/static/password.svg new file mode 100644 index 0000000..d333f60 --- /dev/null +++ b/static/password.svg @@ -0,0 +1,121 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/templates/base.html b/templates/base.html index 733bf1a..26341af 100644 --- a/templates/base.html +++ b/templates/base.html @@ -26,6 +26,15 @@ height: 56px; } + @@ -45,13 +54,22 @@
{% if current_user.is_authenticated %} - - {{ current_user.name }} - - + + + + +
+ + + + +
+ + {% else %} diff --git a/templates/change_pwd.html b/templates/change_pwd.html new file mode 100644 index 0000000..5f4626d --- /dev/null +++ b/templates/change_pwd.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block content %} +

+
+ + + + + + +
+{% endblock %} diff --git a/test.py b/test.py new file mode 100644 index 0000000..ac95761 --- /dev/null +++ b/test.py @@ -0,0 +1,59 @@ +import pickle +from cryptography.fernet import Fernet + +# ===================================================== +# SECRET KEY (only your program has this) +# ===================================================== +# Generate once using: Fernet.generate_key() +_SECRET_KEY = b'YFK7QCyTzhyLO4vqrnRxvDAI5uu8mXEYrInEjbRoQgs=' + +fernet = Fernet(_SECRET_KEY) + +# ===================================================== +# ENCRYPT +# ===================================================== +def encrypt_object(obj) -> bytes: + """ + Encrypt any Python object and return encrypted bytes. + """ + serialized = pickle.dumps(obj) + encrypted = fernet.encrypt(serialized) + return encrypted + +# ===================================================== +# DECRYPT +# ===================================================== +def decrypt_object(encrypted_data: bytes): + """ + Decrypt bytes back into the original Python object. + """ + decrypted = fernet.decrypt(encrypted_data) + obj = pickle.loads(decrypted) + return obj + +# ===================================================== +# EXAMPLE USAGE +# ===================================================== +if __name__ == "__main__": + original_object = { + "user": "alice", + "permissions": ["read", "write"], + "balance": 123.45 + } + + encrypted = encrypt_object(original_object) + print("Encrypted:", str(encrypted)) + + decrypted = decrypt_object(encrypted) + print("Decrypted:", decrypted) + + +bytes_data = b'I am a bytes string' + +string_data = bytes_data.decode('utf-8') + +print("Type Before :- ",type(bytes_data)) + +print(string_data) + +print("Type After :- ",type(string_data)) \ No newline at end of file