added login cookie, added change pwd screen

This commit is contained in:
2026-01-18 08:22:41 +01:00
parent 2d7297e3b7
commit 23631c6d24
10 changed files with 480 additions and 22 deletions
+99
View File
@@ -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
+98 -3
View File
@@ -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")
+20 -9
View File
@@ -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/<listid>")
@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/<itemid>/<itemchecked>")
@@ -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/<listid>", 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/<itemid>/<quantity>")
@@ -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/<itemid>/<category>")
+3 -1
View File
@@ -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)
+11 -4
View File
@@ -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
+29
View File
@@ -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)
+121
View File
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16.482828mm"
height="14.68959mm"
viewBox="0 0 16.482828 14.68959"
version="1.1"
id="svg8634"
inkscape:version="0.92.1 r15371"
sodipodi:docname="password reset.svg">
<defs
id="defs8628" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6568543"
inkscape:cx="-3.8835724"
inkscape:cy="1.3449005"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata8631">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-3.0978698,-4.6612284)">
<g
transform="matrix(1.2809155,0,0,1.2374188,103.99112,-1219.3652)"
id="g4667">
<path
inkscape:connector-curvature="0"
id="path4640"
d="m -72.695687,1000.9938 c -1.972631,-0.2257 -3.790181,-1.49518 -4.687846,-3.27432 -0.175341,-0.34752 -0.468403,-1.12381 -0.468403,-1.24073 0,-0.0266 0.375731,-0.048 0.834956,-0.0476 l 0.834965,7.2e-4 0.163557,0.4041 c 0.519646,1.28389 1.615359,2.24396 2.994703,2.62398 0.49589,0.13662 1.63139,0.13421 2.144474,-0.005 0.522244,-0.14124 1.250991,-0.50448 1.653619,-0.82424 0.687488,-0.546 1.31115,-1.49718 1.539,-2.34722 0.146128,-0.54518 0.177544,-1.43846 0.07041,-2.00269 -0.178809,-0.94189 -0.54809,-1.63735 -1.241568,-2.33827 -0.844669,-0.85372 -1.728666,-1.24944 -2.900391,-1.29837 -0.550547,-0.023 -0.767325,-0.006 -1.18485,0.0941 -1.415428,0.33867 -2.632012,1.39775 -3.122259,2.71804 l -0.103341,0.27832 h -0.842765 -0.842766 l 0.03834,-0.16113 c 0.08466,-0.35604 0.444253,-1.16597 0.699534,-1.57555 0.354919,-0.56944 1.084444,-1.33426 1.637166,-1.71635 3.344531,-2.31207 7.975171,-0.83571 9.268415,2.95499 1.2168,3.56666 -1.256634,7.33132 -5.098284,7.75962 -0.62535,0.07 -0.754641,0.069 -1.386656,0 z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.05859375;stroke-opacity:1" />
<g
id="g4592-5-0"
transform="matrix(0.77578717,0,0,0.61178065,-35.477186,373.83106)">
<rect
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.04824853"
id="rect3717-9-8-2"
width="4.4458065"
height="1.1666708"
x="-55.705212"
y="1017.3687" />
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke-width:0.93749994"
id="path3783-0-6"
sodipodi:sides="3"
sodipodi:cx="-109.71203"
sodipodi:cy="1025.7628"
sodipodi:r1="8.7855453"
sodipodi:r2="6.0797644"
sodipodi:arg1="0.43833656"
sodipodi:arg2="1.4855341"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m -101.75708,1029.4917 -15.16173,1.2959 6.45861,-13.7784 z"
inkscape:transform-center-x="-0.034756175"
inkscape:transform-center-y="0.58213244"
transform="matrix(-0.30321012,-0.03553367,0.03028113,-0.35580483,-117.82847,1375.3296)" />
</g>
</g>
<g
id="g73"
transform="matrix(0.83530458,0,0,1,39.090178,0.46999031)">
<path
inkscape:connector-curvature="0"
id="path68"
d="m -34.487464,10.849979 0.0036,-0.730218 0.02775,-0.1337396 c 0.196159,-0.9452109 0.892942,-1.6292248 1.80383,-1.7707713 0.149269,-0.023195 0.448719,-0.023232 0.597434,-8.14e-5 0.850246,0.1323855 1.532973,0.7494915 1.763181,1.5937089 0.07187,0.2635504 0.07468,0.3040374 0.07468,1.0731994 l 3e-6,0.687422 h -0.324033 -0.324031 v -0.678215 c 0,-0.574261 -0.0024,-0.691927 -0.01605,-0.767667 -0.115574,-0.6441436 -0.601574,-1.1513119 -1.21426,-1.2671477 -0.133279,-0.025199 -0.38006,-0.025995 -0.5063,-0.00165 -0.211637,0.040837 -0.407858,0.1240287 -0.588513,0.2495169 -0.118197,0.082103 -0.323993,0.2988632 -0.400284,0.4216091 -0.103814,0.1670294 -0.170352,0.3247404 -0.218926,0.5189097 l -0.02677,0.106992 -0.0035,0.714169 -0.0035,0.714171 h -0.324083 -0.324083 z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke-width:0.01040864" />
<rect
ry="0.44648439"
y="10.52671"
x="-34.942802"
height="3.3545389"
width="5.1773005"
id="rect12"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke-width:0.25095639" />
<path
inkscape:connector-curvature="0"
id="path39"
d="m -32.915205,12.470548 c -6.38e-4,-0.291161 -3.71e-4,-0.547149 6.25e-4,-0.568863 0.0065,-0.140261 0.0869,-0.294293 0.198787,-0.380573 0.09138,-0.07046 0.187004,-0.106399 0.298919,-0.112341 0.248028,-0.01317 0.470974,0.149576 0.540122,0.394269 0.01125,0.03982 0.01296,0.108928 0.0153,0.620851 l 0.0026,0.576041 h -0.527593 -0.527593 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-width:0.00735304" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+23 -5
View File
@@ -26,6 +26,15 @@
height: 56px;
}
</style>
<script>
function toggleButtons() {
if ($('#toprightbuttons').hasClass("d-none")){
$('#toprightbuttons').removeClass("d-none")
} else {
$('#toprightbuttons').addClass("d-none")
}
}
</script>
</head>
<body class="bg-light">
@@ -45,13 +54,22 @@
<div class="d-flex align-items-center gap-2">
{% if current_user.is_authenticated %}
<span class="text-muted small d-none d-sm-inline">
{{ current_user.name }}
</span>
<a href="{{ url_for('auth.logout') }}"
>
<div class="text-muted small d-none d-sm-inline">
<a class="btn" onclick="toggleButtons()">{{current_user.name }}</a>
</div>
<div id="toprightbuttons" class="d-none">
<a href="{{ url_for('auth.logout') }}">
<img style="width: 2em; height: 2em" src="/static/logout.svg">
</a>
<a href="{{ url_for('auth.change_pwd') }}">
<img style="width: 2em; height: 2em" src="/static/password.svg">
</a>
</div>
{% else %}
<a href="{{ url_for('auth.login') }}"
>
+17
View File
@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<h3 class="text-center mb-4"><img style="width: 3em; height: 3em" src="/static/login.svg"></h3>
<form method="post">
<input class="form-control mb-3" name="password_old" type="password" placeholder="Old Password" required>
<input class="form-control mb-3" name="password_new1" type="password" placeholder="New Password" required>
<input class="form-control mb-3" name="password_new2" type="password" placeholder="Repeat" required>
<input
type="image"
src="/static/save.svg"
alt="Save"
style="width: 2em; height: 2em;"
>
</form>
{% endblock %}
+59
View File
@@ -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))