From 5277830c50511176844ecbe402b402a6c040dff8 Mon Sep 17 00:00:00 2001 From: handi Date: Sun, 30 Nov 2025 16:34:34 +0100 Subject: [PATCH] Add change password --- app/{auth_core.py => auth.py} | 49 +++++++++++++++- app/db.py | 54 ++++++++++++++++++ app/main.py | 32 ++++++++++- app/migrate.py | 3 +- data/app.db | Bin 32768 -> 32768 bytes .../20251130_161100_add_col_newpwd_users.sql | 8 +++ 6 files changed, 143 insertions(+), 3 deletions(-) rename app/{auth_core.py => auth.py} (71%) create mode 100644 migrations/20251130_161100_add_col_newpwd_users.sql diff --git a/app/auth_core.py b/app/auth.py similarity index 71% rename from app/auth_core.py rename to app/auth.py index 7638b66..b3741c0 100644 --- a/app/auth_core.py +++ b/app/auth.py @@ -3,7 +3,7 @@ from contextlib import closing import bcrypt -from db import get_conn +from db import get_conn #, create_user, verify_user, get_role_for_user # --------------------------------------------------------------------------- @@ -133,3 +133,50 @@ def load_credentials_from_db() -> dict: } return creds + +# --------------------------------------------------------------------------- +# Muss das Passwort geändert werden (Admin-Vorgabe)? +# --------------------------------------------------------------------------- + +def needs_password_change(username: str) -> bool: + """Prüft, ob der User ein neues Passwort setzen muss (new_pwd = 1).""" + with closing(get_conn()) as conn: + row = conn.execute( + "SELECT new_pwd FROM users WHERE username = ?", + (username,), + ).fetchone() + if not row: + return False + return row[0] == 1 + +# --------------------------------------------------------------------------- +# Passwort ändern durch den Benutzer +# --------------------------------------------------------------------------- + +def update_password(username: str, new_password: str, reset_flag: bool = True) -> bool: + """ + Setzt ein neues Passwort für den User. + Wenn reset_flag=True: new_pwd wird auf 0 zurückgesetzt. + """ + pw_hash = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + + with closing(get_conn()) as conn, conn: + if reset_flag: + conn.execute( + """ + UPDATE users + SET password_hash = ?, new_pwd = 0 + WHERE username = ? + """, + (pw_hash, username), + ) + else: + conn.execute( + """ + UPDATE users + SET password_hash = ? + WHERE username = ? + """, + (pw_hash, username), + ) + return True \ No newline at end of file diff --git a/app/db.py b/app/db.py index 3a50c13..d5c6b4c 100644 --- a/app/db.py +++ b/app/db.py @@ -1,5 +1,6 @@ import sqlite3 from pathlib import Path +import bcrypt BASE_DIR = Path(__file__).resolve().parent.parent DB_PATH = BASE_DIR / "data" / "app.db" @@ -8,3 +9,56 @@ DB_PATH = BASE_DIR / "data" / "app.db" def get_conn(): # check_same_thread=False, damit Streamlit mehrere Threads nutzen kann return sqlite3.connect(DB_PATH, check_same_thread=False) + +def create_user( + username: str, + password: str, + role: str = "user", + email: str | None = None, + firstname: str | None = None, + lastname: str | None = None +) -> bool: + + """ + Passwort wird als bcrypt-Hash (TEXT) gespeichert. + """ + pw_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + + try: + with closing(get_conn()) as conn, conn: + conn.execute( + "INSERT INTO users (username, email, password_hash, role, firstname, lastname) VALUES (?, ?, ?, ?, ?, ?)", + (username, email, pw_hash, role, firstname, lastname), + ) + return True + except Exception: + return False + +def verify_user(username: str, password: str): + """ + Vergleicht eingegebenes Passwort mit dem gespeicherten bcrypt-Hash. + Rückgabe: (True, role) oder (False, None) + """ + with closing(get_conn()) as conn: + row = conn.execute( + "SELECT password_hash, role FROM users WHERE username = ?", + (username,), + ).fetchone() + + if not row: + return False, None + + stored_hash, role = row + + # stored_hash ist TEXT → zurück nach bytes + ok = bcrypt.checkpw(password.encode("utf-8"), stored_hash.encode("utf-8")) + + return (ok, role) if ok else (False, None) + +def get_role_for_user(username: str) -> str: + with closing(get_conn()) as conn: + row = conn.execute( + "SELECT role FROM users WHERE username = ?", + (username,), + ).fetchone() + return row[0] if row else "user" diff --git a/app/main.py b/app/main.py index 3b6ab32..fa8264a 100644 --- a/app/main.py +++ b/app/main.py @@ -2,7 +2,7 @@ import streamlit as st import yaml from yaml.loader import SafeLoader import streamlit_authenticator as stauth -from auth_core import load_credentials_from_db, get_role_for_user, create_user +from auth import load_credentials_from_db, get_role_for_user, create_user, needs_password_change, update_password from version import __version__ @@ -83,6 +83,36 @@ def main(): return # ---- Ab hier eingeloggt (persistenter Cookie) ---- + + if needs_password_change(username): + st.warning("Du musst dein Passwort ändern, bevor du die Anwendung nutzen kannst.") + + # Damit das Formular nur einmal pro Run erscheint + with st.form("pw_change_form"): + pw1 = st.text_input("Neues Passwort", type="password") + pw2 = st.text_input("Neues Passwort (Wiederholung)", type="password") + submitted = st.form_submit_button("Passwort ändern") + + if submitted: + if not pw1 or not pw2: + st.error("Bitte beide Passwortfelder ausfüllen.") + return + if pw1 != pw2: + st.error("Passwörter stimmen nicht überein.") + return + if len(pw1) < 8: + st.error("Passwort sollte mindestens 8 Zeichen lang sein.") + return + + update_password(username, pw1, reset_flag=True) + st.success("Passwort wurde geändert.") + + # Optional: danach Seite einmal „sauber“ neu laden + st.rerun() + + # Solange new_pwd=1 ist, KEINEN weiteren Content anzeigen + return + role = get_role_for_user(username) authenticator.logout("Logout", "sidebar") diff --git a/app/migrate.py b/app/migrate.py index 20b2f2e..f681fc2 100644 --- a/app/migrate.py +++ b/app/migrate.py @@ -6,7 +6,7 @@ from logging_config import setup_logging import os from contextlib import closing import getpass -from auth_core import create_user +from auth import create_user APP_ENV = os.environ.get("APP_ENV", "dev") logger = setup_logging(APP_ENV) @@ -98,6 +98,7 @@ def create_admin_user(): if admin_exists(): logger.info("Adminkonto existiert bereits! Kein initiales Konto angelegt.") return + logger.info("Adminkonto wird angelegt ...") pw1 = getpass.getpass("Passwort: ") diff --git a/data/app.db b/data/app.db index 43962dc62501ea80da47dd3ef96e5c9c7ebd92f2..258f95c8010ea426d62e7ccd3f261f4329c43a3c 100644 GIT binary patch delta 393 zcmZo@U}|V!njkI6%fP_E0mLxCK2gV5nwLSZ_a`s!M+SCoc?N!I{@vX2d>?q{^OW)C zZERf1#nlwT%q}i2&e+Pn*_T^`QAI}~FSR_rpgct(Gp{5yJ+(+7FTX?~uQVq|Atf~} zu{5Vd!EkdaPd_ssE8hzS{)_z6`D6H1_+D%l6u8D`W5voGYHVa+WNK(=Y!Gi~W@u<& z5TBTm5}%x(6Av^PXl{IIacWU9NR6(cp{}ukf}yFEp^25LWt9Lh+HnXHow^(S!iGcz#pwK4Fu@z(;~RK>?z&B)DQEZ)c}t|}?c zF?oW00ShpIswOs;@ij6qL8T1Efl~bT@}?e+VP=_bAwDH$5ypx7#eSLk-hqXB#lCK( npvUGLFVO-Mv)~cdRZQh`B{aNlkF83IXD0S<}Yab delta 239 zcmZo@U}|V!njkI6#=yY90mLxCHc`h|nvFrP_a`s!M+SCoX9j+0{@vWpd>?q{^OW)C zZB`UG$;H)Vz|1Z#F3#9uzgd%8gK={yPd_sc3%?2j|3&`k{4xA0n-vvA_$RB#KLCmr zGVnj;-^V|Vzi_jlLn8m=Lj4KcK$4l&JeO{`4x@(oW7Hu7>z Z@t%C&UXiDY5o(mGr1<2?b_I(Z8~{pBJKX>P diff --git a/migrations/20251130_161100_add_col_newpwd_users.sql b/migrations/20251130_161100_add_col_newpwd_users.sql new file mode 100644 index 0000000..e6f15c3 --- /dev/null +++ b/migrations/20251130_161100_add_col_newpwd_users.sql @@ -0,0 +1,8 @@ +begin; + +alter table users +add column new_pwd integer not null default 1; + +INSERT INTO schema_version (version) VALUES ('20251130_161100_add_col_newpwd_users'); + +COMMIT; \ No newline at end of file