From e2d96c712ea66907f82e130da26272d9f92baf40 Mon Sep 17 00:00:00 2001 From: handi Date: Wed, 3 Dec 2025 22:03:15 +0100 Subject: [PATCH] Update migrations --- app/.streamlit/config.toml | 2 + app/auth.py | 17 +- app/auth_runtime.py | 70 +++ app/dashboards/Benutzer.py | 13 +- app/dashboards/home.py | 46 ++ app/db.py | 52 -- app/main.py | 486 +---------------- app/main_old.py | 141 ----- app/main_v1.py | 494 ++++++++++++++++++ app/pages/Benutzer.py | 43 ++ app/pages/home.py | 17 + app/ui/sidebar.py | 54 ++ data/app.db | Bin 69632 -> 69632 bytes .../20251130_191100_add_table_dashboards.sql | 7 +- .../20251203_201300_rename_col_table_role.sql | 8 + ...1203_202200_add_col_active_table_users.sql | 8 + .../20251203_202900_add_col_order_tables.sql | 11 + 17 files changed, 786 insertions(+), 683 deletions(-) create mode 100644 app/.streamlit/config.toml create mode 100644 app/auth_runtime.py create mode 100644 app/dashboards/home.py delete mode 100644 app/main_old.py create mode 100644 app/main_v1.py create mode 100644 app/pages/Benutzer.py create mode 100644 app/pages/home.py create mode 100644 app/ui/sidebar.py create mode 100644 migrations/20251203_201300_rename_col_table_role.sql create mode 100644 migrations/20251203_202200_add_col_active_table_users.sql create mode 100644 migrations/20251203_202900_add_col_order_tables.sql diff --git a/app/.streamlit/config.toml b/app/.streamlit/config.toml new file mode 100644 index 0000000..74812cd --- /dev/null +++ b/app/.streamlit/config.toml @@ -0,0 +1,2 @@ +[client] +showSidebarNavigation = false \ No newline at end of file diff --git a/app/auth.py b/app/auth.py index 376d090..de09f6b 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,4 +1,3 @@ -# app/auth_core.py from contextlib import closing import bcrypt @@ -13,7 +12,7 @@ from db import get_conn #, create_user, verify_user, get_role_for_user def create_user( username: str, password: str, - role: str = "user", + role_id: str = "user", email: str | None = None, firstname: str | None = None, lastname: str | None = None @@ -27,8 +26,8 @@ def create_user( 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), + "INSERT INTO users (username, email, password_hash, role_id, firstname, lastname) VALUES (?, ?, ?, ?, ?, ?)", + (username, email, pw_hash, role_id, firstname, lastname), ) return True except Exception: @@ -42,23 +41,23 @@ def create_user( def verify_user(username: str, password: str): """ Vergleicht eingegebenes Passwort mit dem gespeicherten bcrypt-Hash. - Rückgabe: (True, role) oder (False, None) + Rückgabe: (True, role_id) oder (False, None) """ with closing(get_conn()) as conn: row = conn.execute( - "SELECT password_hash, role FROM users WHERE username = ?", + "SELECT password_hash, role_id FROM users WHERE username = ?", (username,), ).fetchone() if not row: return False, None - stored_hash, role = row + stored_hash, role_id = 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) + return (ok, role_id) if ok else (False, None) # --------------------------------------------------------------------------- @@ -68,7 +67,7 @@ def verify_user(username: str, password: str): def get_role_for_user(username: str) -> str: with closing(get_conn()) as conn: row = conn.execute( - "SELECT role FROM users WHERE username = ?", + "SELECT role_id FROM users WHERE username = ?", (username,), ).fetchone() return row[0] if row else "user" diff --git a/app/auth_runtime.py b/app/auth_runtime.py new file mode 100644 index 0000000..7223142 --- /dev/null +++ b/app/auth_runtime.py @@ -0,0 +1,70 @@ +import streamlit as st +import yaml +from yaml.loader import SafeLoader +import streamlit_authenticator as stauth +from streamlit_authenticator.utilities.exceptions import LoginError + +from auth import load_credentials_from_db, needs_password_change, update_password + +def get_authenticator(): + with open("config/auth.yaml", "r", encoding="utf-8") as f: + base_config = yaml.load(f, Loader=SafeLoader) + db_creds = load_credentials_from_db() + base_config["credentials"] = db_creds + + authenticator = stauth.Authenticate( + base_config["credentials"], + base_config["cookie"]["name"], + base_config["cookie"]["key"], + base_config["cookie"]["expiry_days"], + ) + return authenticator + + +def require_login(): + """Sicherstellen, dass der User eingeloggt ist. Auf JEDER Page verwenden.""" + authenticator = get_authenticator() + + try: + authenticator.login(location="main", key="Login") + except LoginError: + authenticator.logout("ForceLogout", "sidebar") + st.error("Sitzung ungültig. Bitte neu einloggen.") + st.stop() + + auth_status = st.session_state.get("authentication_status") + if auth_status is False: + st.error("Login fehlgeschlagen.") + st.stop() + if auth_status is None: + st.warning("Bitte Benutzername und Passwort eingeben.") + st.stop() + + # Passwortwechsel erzwingen + username = st.session_state.get("username") + if needs_password_change(username): + st.warning("Du musst dein Passwort ändern, bevor du die Anwendung nutzen kannst.") + + 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.") + st.stop() + if pw1 != pw2: + st.error("Passwörter stimmen nicht überein.") + st.stop() + if len(pw1) < 8: + st.error("Passwort sollte mindestens 8 Zeichen lang sein.") + st.stop() + + update_password(username, pw1, reset_flag=True) + st.success("Passwort wurde geändert.") + st.rerun() + + st.stop() + + return authenticator \ No newline at end of file diff --git a/app/dashboards/Benutzer.py b/app/dashboards/Benutzer.py index f0ea140..c2a9966 100644 --- a/app/dashboards/Benutzer.py +++ b/app/dashboards/Benutzer.py @@ -1,6 +1,17 @@ import streamlit as st -from auth import create_user +from auth_runtime import require_login +from ui.sidebar import build_sidebar, hide_sidebar_if_logged_out +from auth import get_fullname_for_user + +hide_sidebar_if_logged_out() + +st.set_page_config(page_title="Co-App Home", page_icon="🏠") + +authenticator = require_login() +st.session_state["authenticator"] = authenticator + +build_sidebar() def render(username: str, role: str): st.title("Benutzerverwaltung") diff --git a/app/dashboards/home.py b/app/dashboards/home.py new file mode 100644 index 0000000..39130cc --- /dev/null +++ b/app/dashboards/home.py @@ -0,0 +1,46 @@ +import streamlit as st +from auth import create_user, get_fullname_for_user, get_role_for_user +from ui.sidebar import build_sidebar + +build_sidebar + +st.header("Controlling-Portal") +st.info(f"Willkommen, {username}!") + + + + + + + + + # if role == "admin": + # st.subheader("Admin-Bereich") + # st.write("Nur Admins sehen das hier.") + + # with st.expander("Neuen Nutzer anlegen"): + # new_u = st.text_input("Neuer Username", key="new_u") + # new_fname = st.text_input("Vorname", key="new_fname") + # new_lname = st.text_input("Nachname", key="new_lname") + # new_email = st.text_input("E-Mail", key="new_email") + # new_p = st.text_input("Neues Passwort", type="password", key="new_p") + # new_role = st.selectbox("Rolle", ["user", "admin"], key="new_role") + + # if st.button("Anlegen"): + # if new_u and new_p: + # ok = create_user( + # new_u.strip(), + # new_p, + # new_role, + # new_email.strip() or None, + # new_fname.strip() or None, + # new_lname.strip() or None, + # ) + # st.success("Nutzer angelegt.") if ok else st.error( + # "Username bereits vorhanden oder Fehler." + # ) + # else: + # st.warning("Bitte Username und Passwort eingeben.") + + # st.subheader("Dein Bereich") + # st.write(f"Personalisierter Content für **{username}**.") diff --git a/app/db.py b/app/db.py index d5c6b4c..5c7b5c2 100644 --- a/app/db.py +++ b/app/db.py @@ -10,55 +10,3 @@ 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 86fd972..0e8efcd 100644 --- a/app/main.py +++ b/app/main.py @@ -1,186 +1,7 @@ -import importlib - import streamlit as st -import yaml -from yaml.loader import SafeLoader -import streamlit_authenticator as stauth -from streamlit_authenticator.utilities.exceptions import LoginError - -import pandas as pd -from contextlib import closing - -from auth import ( - load_credentials_from_db, - get_role_for_user, - create_user, - needs_password_change, - update_password, - get_fullname_for_user, -) from version import __version__ -from db import get_conn # an deine Struktur anpassen - - -# --------------------------------------------------------------------------- -# Dashboards aus der DB laden -# --------------------------------------------------------------------------- - -def load_dashboards_df() -> pd.DataFrame: - """Lädt alle aktiven Dashboards aus der DB als DataFrame.""" - with closing(get_conn()) as conn: - df = pd.read_sql_query( - """ - SELECT - d.dash_id, - d.dash_text as code, - d.dash_text as title, - g.group_text as grp - from - dashboards d - left join groups g - on d.group_id = g.group_id - where - d.active = 1 - order by - d.dash_id - """, - conn, - ) - return df - - -# --------------------------------------------------------------------------- -# Home-Dashboard (als Funktion, kein eigenes .py nötig) -# --------------------------------------------------------------------------- - -def home_dashboard(username: str, role: str): - st.header("Controlling-Portal") - st.info(f"Willkommen, {username}!") - - if role == "admin": - st.subheader("Admin-Bereich") - st.write("Nur Admins sehen das hier.") - - with st.expander("Neuen Nutzer anlegen"): - new_u = st.text_input("Neuer Username", key="new_u") - new_fname = st.text_input("Vorname", key="new_fname") - new_lname = st.text_input("Nachname", key="new_lname") - new_email = st.text_input("E-Mail", key="new_email") - new_p = st.text_input("Neues Passwort", type="password", key="new_p") - new_role = st.selectbox("Rolle", ["user", "admin"], key="new_role") - - if st.button("Anlegen"): - if new_u and new_p: - ok = create_user( - new_u.strip(), - new_p, - new_role, - new_email.strip() or None, - new_fname.strip() or None, - new_lname.strip() or None, - ) - st.success("Nutzer angelegt.") if ok else st.error( - "Username bereits vorhanden oder Fehler." - ) - else: - st.warning("Bitte Username und Passwort eingeben.") - - st.subheader("Dein Bereich") - st.write(f"Personalisierter Content für **{username}**.") - - -# --------------------------------------------------------------------------- -# Dashboard-Resolver: code -> render-Funktion -# --------------------------------------------------------------------------- - -def get_dashboard_renderer(code: str): - """ - Liefert die passende Render-Funktion zum Dashboard-Code. - Konvention: - - Home: interne Funktion home_dashboard(username, role) - - andere: Modul dashboards. mit Funktion render(username, role) - """ - if code == "home": - return home_dashboard - - module_name = f"dashboards.{code}" - try: - module = importlib.import_module(module_name) - except ModuleNotFoundError: - return None - - # Wir erwarten eine Funktion render(username, role) - return getattr(module, "render", None) - - -# --------------------------------------------------------------------------- -# Sidebar: Home + Suche + DB-Dashboards (Gruppen-Expander) -# --------------------------------------------------------------------------- - -def build_sidebar(authenticator, username: str, df_dashboards: pd.DataFrame): - role = get_role_for_user(username) - fullname = get_fullname_for_user(username) - - with st.sidebar: - st.logo("app/images/GMN_Logo_neu_rgb.png",size="small") - # st.markdown("*Controlling*") - st.write(f"**{fullname}** ({role})") - - col1, col2 = st.columns(2) - - with col1: - if st.button("Logout", use_container_width=True): - authenticator.logout("Logout", "unrendered") - st.rerun() - #authenticator.logout("Logout", "sidebar") - with col2: - if st.button("🏠 Home", use_container_width=True): - st.session_state["selected_dashboard_code"] = "home" - # st.divider() - st.markdown("### Dashboards") - # st.divider() - - # Default-Auswahl - if "selected_dashboard_code" not in st.session_state: - st.session_state["selected_dashboard_code"] = "home" - - # Home ist immer da - # if st.button("🏠 Home", use_container_width=True): - # st.session_state["selected_dashboard_code"] = "home" - - # st.markdown("---") - - # Suchfeld für DB-Dashboards - query = st.text_input("Suche", placeholder="Dashboard suchen …") - - filtered = df_dashboards.copy() - - if query: - q = query.lower() - filtered = filtered[ - filtered["title"].str.lower().str.contains(q, na=False) - | filtered["grp"].str.lower().str.contains(q, na=False) - | filtered["description"].fillna("").str.lower().str.contains(q, na=False) - ] - - if filtered.empty: - st.info("Keine Dashboards zur Suche gefunden.") - return - - # Gruppen -> Expander, darin Buttons für Dashboards - for grp, grp_df in filtered.groupby("grp"): - with st.expander(grp, expanded=False): - for _, row in grp_df.iterrows(): - code = row["code"] - title = row["title"] - - if st.button(title, key=f"dash_btn_{code}", use_container_width=True): - st.session_state["selected_dashboard_code"] = code - - -# --------------------------------------------------------------------------- -# Login + App-Shell -# --------------------------------------------------------------------------- +from auth_runtime import require_login +from ui.sidebar import build_sidebar def main(): st.set_page_config( @@ -189,303 +10,14 @@ def main(): layout="centered", ) - # --- Authenticator vorbereiten --- - with open("config/auth.yaml", "r", encoding="utf-8") as f: - base_config = yaml.load(f, Loader=SafeLoader) + authenticator = require_login() + # damit build_sidebar den authenticator findet: + st.session_state["authenticator"] = authenticator - db_creds = load_credentials_from_db() - base_config["credentials"] = db_creds - - authenticator = stauth.Authenticate( - base_config["credentials"], - base_config["cookie"]["name"], - base_config["cookie"]["key"], - base_config["cookie"]["expiry_days"], - ) - - # --- Login-View zeichnen --- - try: - authenticator.login(location="main", key="Login") - except LoginError: - authenticator.logout("ForceLogout", "sidebar") - st.error("Sitzung ungültig. Bitte neu einloggen.") - return - - auth_status = st.session_state.get("authentication_status") - name = st.session_state.get("name") - username = st.session_state.get("username") - - if auth_status is False: - st.error("Login fehlgeschlagen.") - return - - if auth_status is None: - st.warning("Bitte Benutzername und Passwort eingeben.") - return - - # ---- Ab hier eingeloggt (persistenter Cookie) ---- - - # 1) Passwortwechsel erzwingen? - if needs_password_change(username): - st.warning("Du musst dein Passwort ändern, bevor du die Anwendung nutzen kannst.") - - 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.") - st.rerun() - - # solange new_pwd=1: keinen weiteren Inhalt anzeigen - return - - # 2) Dashboards aus DB laden (ohne Home) - df_dashboards = load_dashboards_df() - - # 3) Sidebar aufbauen (User, Logout, Navigation) - build_sidebar(authenticator, username, df_dashboards) - - # 4) Ausgewähltes Dashboard rendern - role = get_role_for_user(username) - selected_code = st.session_state.get("selected_dashboard_code", "home") - renderer = get_dashboard_renderer(selected_code) - - if renderer is None: - st.error(f"Kein Dashboard-Modul für '{selected_code}' gefunden.") - return - - # Titel setzen - if selected_code == "home": - title = "Home" - else: - row = df_dashboards[df_dashboards["code"] == selected_code] - title = row["title"].iloc[0] if not row.empty else selected_code - - st.title(title) - renderer(username, role) + build_sidebar() + st.header("Controlling-Portal") + st.info(f"Willkommen, {st.session_state.get('username')}!") if __name__ == "__main__": - main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# import importlib - -# import streamlit as st -# import yaml -# from yaml.loader import SafeLoader -# import streamlit_authenticator as stauth -# from streamlit_authenticator.utilities.exceptions import LoginError - -# from auth import ( -# load_credentials_from_db, -# get_role_for_user, -# create_user, -# needs_password_change, -# update_password, -# get_fullname_for_user, -# ) -# from version import __version__ -# # später: from db import get_conn, um Dashboards aus DB zu laden - - -# # --------------------------------------------------------------------------- -# # Home-Dashboard (als Funktion, kein eigenes .py nötig) -# # --------------------------------------------------------------------------- - -# def home_dashboard(username: str, role: str): -# # st.header("Controlling-Portal") -# st.info(f"Willkommen, {username}!") - - - - -# # --------------------------------------------------------------------------- -# # Dashboard-Resolver (später: weitere Dashboards dynamisch laden) -# # --------------------------------------------------------------------------- - -# def get_dashboard_renderer(code: str): -# """Liefert die passende Render-Funktion zum Dashboard-Code.""" -# if code == "home": -# # internes Home-Dashboard -# return home_dashboard - -# # Später: Module anhand 'code' aus dashboards-Package laden -# # Beispiel: dashboards/sales.py → code = "sales" -# try: -# module = importlib.import_module(f"dashboards.{code}") -# except ModuleNotFoundError: -# return None - -# return getattr(module, "render", None) - - -# # --------------------------------------------------------------------------- -# # Sidebar – Navigation (Home + später DB-Dashboards) -# # --------------------------------------------------------------------------- - -# def build_sidebar(authenticator, username: str): -# role = get_role_for_user(username) -# fullname = get_fullname_for_user(username) - -# with st.sidebar: -# st.write(f"**{fullname}** ({role})") -# authenticator.logout("Logout", "sidebar") -# st.divider() - -# # Default-Auswahl -# if "selected_dashboard_code" not in st.session_state: -# st.session_state["selected_dashboard_code"] = "home" - -# # Home ist immer da -# if st.button("🏠 Home", use_container_width=True): -# st.session_state["selected_dashboard_code"] = "home" - -# # Platzhalter für Suche + DB-Dashboards: -# # hier später: Suchfeld + Expander aus DB-Result (DataFrame) -# # z.B.: -# # query = st.text_input("Dashboards suchen …") -# # df = load_dashboards_df() -# # ... filtern, gruppieren, Buttons setzen ... - - -# # --------------------------------------------------------------------------- -# # Login + App-Shell -# # --------------------------------------------------------------------------- - -# def main(): -# st.set_page_config( -# page_title=f"Co-App Start - V{__version__}", -# page_icon="🔒", -# layout="centered", -# ) - -# # --- Authenticator vorbereiten --- -# with open("config/auth.yaml", "r", encoding="utf-8") as f: -# base_config = yaml.load(f, Loader=SafeLoader) - -# db_creds = load_credentials_from_db() -# base_config["credentials"] = db_creds - -# authenticator = stauth.Authenticate( -# base_config["credentials"], -# base_config["cookie"]["name"], -# base_config["cookie"]["key"], -# base_config["cookie"]["expiry_days"], -# ) - -# # --- Login-View zeichnen --- -# try: -# authenticator.login(location="main", key="Login") -# except LoginError: -# authenticator.logout("ForceLogout", "sidebar") -# st.error("Sitzung ungültig. Bitte neu einloggen.") -# return - -# auth_status = st.session_state.get("authentication_status") -# name = st.session_state.get("name") -# username = st.session_state.get("username") - -# if auth_status is False: -# st.error("Login fehlgeschlagen.") -# return - -# if auth_status is None: -# st.warning("Bitte Benutzername und Passwort eingeben.") -# return - -# # ---- Ab hier eingeloggt (persistenter Cookie) ---- - -# # 1) Passwortwechsel erzwingen? -# if needs_password_change(username): -# st.warning("Du musst dein Passwort ändern, bevor du die Anwendung nutzen kannst.") - -# 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.") -# st.rerun() - -# # solange new_pwd=1: keinen weiteren Inhalt anzeigen -# return - -# # 2) Sidebar aufbauen (User, Logout, Navigation) -# build_sidebar(authenticator, username) - -# # 3) Ausgewähltes Dashboard rendern -# role = get_role_for_user(username) -# selected_code = st.session_state.get("selected_dashboard_code", "home") -# renderer = get_dashboard_renderer(selected_code) - -# if renderer is None: -# st.error(f"Kein Dashboard-Modul für '{selected_code}' gefunden.") -# return - -# # Titel setzen (Home vs. andere Dashboards) -# if selected_code == "home": -# title = "Home" -# else: -# title = selected_code # später kannst du den Titel aus der DB holen - -# st.title(title) -# renderer(username, role) - - -# if __name__ == "__main__": -# main() + main() \ No newline at end of file diff --git a/app/main_old.py b/app/main_old.py deleted file mode 100644 index 07152b9..0000000 --- a/app/main_old.py +++ /dev/null @@ -1,141 +0,0 @@ -import streamlit as st -import yaml -from yaml.loader import SafeLoader -import streamlit_authenticator as stauth -from auth import load_credentials_from_db, get_role_for_user, create_user, needs_password_change, update_password, get_fullname_for_user -from version import __version__ -from dashboards import get_dashboard_renderer - - - -# --------------------------------------------------------------------------- -# Login beim Starten der App -# --------------------------------------------------------------------------- - -def login(): - st.set_page_config( - page_title=f"Co-App Start - V{__version__}", - page_icon="🔒", - layout="centered", - ) - - # --- Config laden (Cookie, etc.) --- - with open("config/auth.yaml", "r", encoding="utf-8") as f: - base_config = yaml.load(f, Loader=SafeLoader) - - # --- Credentials dynamisch aus DB laden --- - db_creds = load_credentials_from_db() - base_config["credentials"] = db_creds - - authenticator = stauth.Authenticate( - base_config["credentials"], - base_config["cookie"]["name"], - base_config["cookie"]["key"], - base_config["cookie"]["expiry_days"], - ) - - authenticator.login(location="main", key="Login") - - auth_status = st.session_state.get("authentication_status") - name = st.session_state.get("name") - username = st.session_state.get("username") - - if auth_status is False: - st.error("Login fehlgeschlagen.") - return - - if auth_status is None: - st.warning("Bitte Benutzername und Passwort eingeben.") - 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 - - authenticator.logout("Logout", "sidebar") - load_sidebar(username) - -# def home(username: str, role: str): -# st.header("Controlling-Portal") -# st.info(f"Willkommen, {username}!") -# if role == "admin": -# st.subheader("Admin-Bereich") -# st.write("Nur Admins sehen das hier.") - -# with st.expander("Neuen Nutzer anlegen"): -# new_u = st.text_input("Neuer Username", key="new_u") -# new_fname = st.text_input("Vorname", key="new_fname") -# new_lname = st.text_input("Nachname", key="new_lname") -# new_email = st.text_input("E-Mail", key="new_email") -# new_p = st.text_input("Neues Passwort", type="password", key="new_p") -# new_role = st.selectbox("Rolle", ["user", "admin"], key="new_role") - -# if st.button("Anlegen"): -# if new_u and new_p: -# ok = create_user(new_u.strip(), new_p, new_role, new_email, new_fname, new_lname) -# st.success("Nutzer angelegt.") if ok else st.error("Username bereits vorhanden oder Fehler.") -# else: -# st.warning("Bitte Username und Passwort eingeben.") - -# st.subheader("Dein Bereich") -# st.write(f"Personalisierter Content für **{username}**.") - - - - -# --------------------------------------------------------------------------- -# Sidebar aufbauen - Daten aus app.db laden und gruppieren -# --------------------------------------------------------------------------- - - - -def load_sidebar(username: str): - renderer = get_dashboard_renderer(selected_code) - if renderer is None: - st.error(f"Kein Dashboard-Modul für '{selected_code}' gefunden.") - return - - renderer() - - role = get_role_for_user(username) - fullname = get_fullname_for_user(username) - - with st.sidebar: - st.write(f"**{fullname}** ({role})") - st.divider() - st.markdown("### Dashboards") - st.divider() - st.page_link("home.py", label="Home") - - - home(username, role) - -if __name__ == "__main__": - login() diff --git a/app/main_v1.py b/app/main_v1.py new file mode 100644 index 0000000..dab78b6 --- /dev/null +++ b/app/main_v1.py @@ -0,0 +1,494 @@ +import importlib + +import streamlit as st +import yaml +from yaml.loader import SafeLoader +import streamlit_authenticator as stauth +from streamlit_authenticator.utilities.exceptions import LoginError + +import pandas as pd +from contextlib import closing + +from auth import ( + load_credentials_from_db, + get_role_for_user, + create_user, + needs_password_change, + update_password, + get_fullname_for_user, +) +from version import __version__ +from db import get_conn # an deine Struktur anpassen + + + +# --------------------------------------------------------------------------- +# Dashboards aus der DB laden +# --------------------------------------------------------------------------- + +def load_dashboards_df() -> pd.DataFrame: + """Lädt alle aktiven Dashboards aus der DB als DataFrame.""" + with closing(get_conn()) as conn: + df = pd.read_sql_query( + """ + SELECT + d.dash_id, + d.dash_text as code, + d.dash_text as title, + g.group_text as grp + from + dashboards d + left join groups g + on d.group_id = g.group_id + where + d.active = 1 + order by + d.dash_id + """, + conn, + ) + return df + + +# --------------------------------------------------------------------------- +# Home-Dashboard (als Funktion, kein eigenes .py nötig) +# --------------------------------------------------------------------------- + +def home_dashboard(username: str, role: str): + + st.header("Controlling-Portal") + st.info(f"Willkommen, {username}!") + + if role == "admin": + st.subheader("Admin-Bereich") + st.write("Nur Admins sehen das hier.") + + with st.expander("Neuen Nutzer anlegen"): + new_u = st.text_input("Neuer Username", key="new_u") + new_fname = st.text_input("Vorname", key="new_fname") + new_lname = st.text_input("Nachname", key="new_lname") + new_email = st.text_input("E-Mail", key="new_email") + new_p = st.text_input("Neues Passwort", type="password", key="new_p") + new_role = st.selectbox("Rolle", ["user", "admin"], key="new_role") + + if st.button("Anlegen"): + if new_u and new_p: + ok = create_user( + new_u.strip(), + new_p, + new_role, + new_email.strip() or None, + new_fname.strip() or None, + new_lname.strip() or None, + ) + st.success("Nutzer angelegt.") if ok else st.error( + "Username bereits vorhanden oder Fehler." + ) + else: + st.warning("Bitte Username und Passwort eingeben.") + + st.subheader("Dein Bereich") + st.write(f"Personalisierter Content für **{username}**.") + + +# --------------------------------------------------------------------------- +# Dashboard-Resolver: code -> render-Funktion +# --------------------------------------------------------------------------- + +def get_dashboard_renderer(code: str): + """ + Liefert die passende Render-Funktion zum Dashboard-Code. + Konvention: + - Home: interne Funktion home_dashboard(username, role) + - andere: Modul dashboards. mit Funktion render(username, role) + """ + if code == "home": + return home_dashboard + + module_name = f"dashboards.{code}" + try: + module = importlib.import_module(module_name) + except ModuleNotFoundError: + return None + + # Wir erwarten eine Funktion render(username, role) + return getattr(module, "render", None) + + +# --------------------------------------------------------------------------- +# Sidebar: Home + Suche + DB-Dashboards (Gruppen-Expander) +# --------------------------------------------------------------------------- + +def build_sidebar(authenticator, username: str, df_dashboards: pd.DataFrame): + role = get_role_for_user(username) + fullname = get_fullname_for_user(username) + + with st.sidebar: + st.logo("app/images/GMN_Logo_neu_rgb.png",size="small") + # st.markdown("*Controlling*") + st.write(f"**{fullname}** ({role})") + + col1, col2 = st.columns(2) + + with col1: + if st.button("Logout", use_container_width=True): + authenticator.logout("Logout", "unrendered") + st.rerun() + #authenticator.logout("Logout", "sidebar") + with col2: + # st.page_link("pages/home.py", label="Home", icon="🏠") + if st.button("🏠 Home", use_container_width=True): + st.session_state["selected_dashboard_code"] = "home" + # st.divider() + st.markdown("### Dashboards") + # st.divider() + + # Default-Auswahl + if "selected_dashboard_code" not in st.session_state: + st.session_state["selected_dashboard_code"] = "home" + + # Home ist immer da + # if st.button("🏠 Home", use_container_width=True): + # st.session_state["selected_dashboard_code"] = "home" + + # st.markdown("---") + + # Suchfeld für DB-Dashboards + query = st.text_input("Suche", placeholder="Dashboard suchen …") + + filtered = df_dashboards.copy() + + if query: + q = query.lower() + filtered = filtered[ + filtered["title"].str.lower().str.contains(q, na=False) + | filtered["grp"].str.lower().str.contains(q, na=False) + | filtered["description"].fillna("").str.lower().str.contains(q, na=False) + ] + + if filtered.empty: + st.info("Keine Dashboards zur Suche gefunden.") + return + + # Gruppen -> Expander, darin Buttons für Dashboards + for grp, grp_df in filtered.groupby("grp"): + with st.expander(grp, expanded=False): + for _, row in grp_df.iterrows(): + code = row["code"] + title = row["title"] + + if st.button(title, key=f"dash_btn_{code}", use_container_width=True): + st.session_state["selected_dashboard_code"] = code + + +# --------------------------------------------------------------------------- +# Login + App-Shell +# --------------------------------------------------------------------------- + +def main(): + st.set_page_config( + page_title=f"Co-App Start - V{__version__}", + page_icon="🔒", + layout="centered", + ) + + # --- Authenticator vorbereiten --- + with open("config/auth.yaml", "r", encoding="utf-8") as f: + base_config = yaml.load(f, Loader=SafeLoader) + + db_creds = load_credentials_from_db() + base_config["credentials"] = db_creds + + authenticator = stauth.Authenticate( + base_config["credentials"], + base_config["cookie"]["name"], + base_config["cookie"]["key"], + base_config["cookie"]["expiry_days"], + ) + + # --- Login-View zeichnen --- + try: + authenticator.login(location="main", key="Login") + except LoginError: + authenticator.logout("ForceLogout", "sidebar") + st.error("Sitzung ungültig. Bitte neu einloggen.") + return + + auth_status = st.session_state.get("authentication_status") + name = st.session_state.get("name") + username = st.session_state.get("username") + + if auth_status is False: + st.error("Login fehlgeschlagen.") + return + + if auth_status is None: + st.warning("Bitte Benutzername und Passwort eingeben.") + return + + # ---- Ab hier eingeloggt (persistenter Cookie) ---- + + # 1) Passwortwechsel erzwingen? + if needs_password_change(username): + st.warning("Du musst dein Passwort ändern, bevor du die Anwendung nutzen kannst.") + + 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.") + st.rerun() + + # solange new_pwd=1: keinen weiteren Inhalt anzeigen + return + + # 2) Dashboards aus DB laden (ohne Home) + df_dashboards = load_dashboards_df() + + # 3) Sidebar aufbauen (User, Logout, Navigation) + build_sidebar(authenticator, username, df_dashboards) + + # 4) Ausgewähltes Dashboard rendern + role = get_role_for_user(username) + selected_code = st.session_state.get("selected_dashboard_code", "home") + renderer = get_dashboard_renderer(selected_code) + + if renderer is None: + st.error(f"Kein Dashboard-Modul für '{selected_code}' gefunden.") + return + + # Titel setzen + if selected_code == "home": + title = "Home" + else: + row = df_dashboards[df_dashboards["code"] == selected_code] + title = row["title"].iloc[0] if not row.empty else selected_code + + st.title(title) + renderer(username, role) + + +if __name__ == "__main__": + main() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# import importlib + +# import streamlit as st +# import yaml +# from yaml.loader import SafeLoader +# import streamlit_authenticator as stauth +# from streamlit_authenticator.utilities.exceptions import LoginError + +# from auth import ( +# load_credentials_from_db, +# get_role_for_user, +# create_user, +# needs_password_change, +# update_password, +# get_fullname_for_user, +# ) +# from version import __version__ +# # später: from db import get_conn, um Dashboards aus DB zu laden + + +# # --------------------------------------------------------------------------- +# # Home-Dashboard (als Funktion, kein eigenes .py nötig) +# # --------------------------------------------------------------------------- + +# def home_dashboard(username: str, role: str): +# # st.header("Controlling-Portal") +# st.info(f"Willkommen, {username}!") + + + + +# # --------------------------------------------------------------------------- +# # Dashboard-Resolver (später: weitere Dashboards dynamisch laden) +# # --------------------------------------------------------------------------- + +# def get_dashboard_renderer(code: str): +# """Liefert die passende Render-Funktion zum Dashboard-Code.""" +# if code == "home": +# # internes Home-Dashboard +# return home_dashboard + +# # Später: Module anhand 'code' aus dashboards-Package laden +# # Beispiel: dashboards/sales.py → code = "sales" +# try: +# module = importlib.import_module(f"dashboards.{code}") +# except ModuleNotFoundError: +# return None + +# return getattr(module, "render", None) + + +# # --------------------------------------------------------------------------- +# # Sidebar – Navigation (Home + später DB-Dashboards) +# # --------------------------------------------------------------------------- + +# def build_sidebar(authenticator, username: str): +# role = get_role_for_user(username) +# fullname = get_fullname_for_user(username) + +# with st.sidebar: +# st.write(f"**{fullname}** ({role})") +# authenticator.logout("Logout", "sidebar") +# st.divider() + +# # Default-Auswahl +# if "selected_dashboard_code" not in st.session_state: +# st.session_state["selected_dashboard_code"] = "home" + +# # Home ist immer da +# if st.button("🏠 Home", use_container_width=True): +# st.session_state["selected_dashboard_code"] = "home" + +# # Platzhalter für Suche + DB-Dashboards: +# # hier später: Suchfeld + Expander aus DB-Result (DataFrame) +# # z.B.: +# # query = st.text_input("Dashboards suchen …") +# # df = load_dashboards_df() +# # ... filtern, gruppieren, Buttons setzen ... + + +# # --------------------------------------------------------------------------- +# # Login + App-Shell +# # --------------------------------------------------------------------------- + +# def main(): +# st.set_page_config( +# page_title=f"Co-App Start - V{__version__}", +# page_icon="🔒", +# layout="centered", +# ) + +# # --- Authenticator vorbereiten --- +# with open("config/auth.yaml", "r", encoding="utf-8") as f: +# base_config = yaml.load(f, Loader=SafeLoader) + +# db_creds = load_credentials_from_db() +# base_config["credentials"] = db_creds + +# authenticator = stauth.Authenticate( +# base_config["credentials"], +# base_config["cookie"]["name"], +# base_config["cookie"]["key"], +# base_config["cookie"]["expiry_days"], +# ) + +# # --- Login-View zeichnen --- +# try: +# authenticator.login(location="main", key="Login") +# except LoginError: +# authenticator.logout("ForceLogout", "sidebar") +# st.error("Sitzung ungültig. Bitte neu einloggen.") +# return + +# auth_status = st.session_state.get("authentication_status") +# name = st.session_state.get("name") +# username = st.session_state.get("username") + +# if auth_status is False: +# st.error("Login fehlgeschlagen.") +# return + +# if auth_status is None: +# st.warning("Bitte Benutzername und Passwort eingeben.") +# return + +# # ---- Ab hier eingeloggt (persistenter Cookie) ---- + +# # 1) Passwortwechsel erzwingen? +# if needs_password_change(username): +# st.warning("Du musst dein Passwort ändern, bevor du die Anwendung nutzen kannst.") + +# 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.") +# st.rerun() + +# # solange new_pwd=1: keinen weiteren Inhalt anzeigen +# return + +# # 2) Sidebar aufbauen (User, Logout, Navigation) +# build_sidebar(authenticator, username) + +# # 3) Ausgewähltes Dashboard rendern +# role = get_role_for_user(username) +# selected_code = st.session_state.get("selected_dashboard_code", "home") +# renderer = get_dashboard_renderer(selected_code) + +# if renderer is None: +# st.error(f"Kein Dashboard-Modul für '{selected_code}' gefunden.") +# return + +# # Titel setzen (Home vs. andere Dashboards) +# if selected_code == "home": +# title = "Home" +# else: +# title = selected_code # später kannst du den Titel aus der DB holen + +# st.title(title) +# renderer(username, role) + + +# if __name__ == "__main__": +# main() diff --git a/app/pages/Benutzer.py b/app/pages/Benutzer.py new file mode 100644 index 0000000..450f099 --- /dev/null +++ b/app/pages/Benutzer.py @@ -0,0 +1,43 @@ +import streamlit as st +from auth_runtime import require_login +from ui.sidebar import build_sidebar, hide_sidebar_if_logged_out +from auth import get_fullname_for_user, create_user + +hide_sidebar_if_logged_out() + +st.set_page_config(page_title="Co-App Home", page_icon="🏠") + +authenticator = require_login() +st.session_state["authenticator"] = authenticator +username = st.session_state.get("username") + +build_sidebar() + +st.title("Benutzerverwaltung") + +with st.expander("Neuen Nutzer anlegen"): + new_u = st.text_input("Neuer Username", key="new_u") + new_fname = st.text_input("Vorname", key="new_fname") + new_lname = st.text_input("Nachname", key="new_lname") + new_email = st.text_input("E-Mail", key="new_email") + new_p = st.text_input("Neues Passwort", type="password", key="new_p") + new_role = st.selectbox("Rolle", ["user", "admin"], key="new_role") + + if st.button("Anlegen"): + if new_u and new_p: + ok = create_user( + new_u.strip(), + new_p, + new_role, + new_email.strip() or None, + new_fname.strip() or None, + new_lname.strip() or None, + ) + st.success("Nutzer angelegt.") if ok else st.error( + "Username bereits vorhanden oder Fehler." + ) + else: + st.warning("Bitte Username und Passwort eingeben.") + + st.subheader("Dein Bereich") + st.write(f"Personalisierter Content für **{username}**.") diff --git a/app/pages/home.py b/app/pages/home.py new file mode 100644 index 0000000..3a973db --- /dev/null +++ b/app/pages/home.py @@ -0,0 +1,17 @@ +import streamlit as st +from auth_runtime import require_login +from ui.sidebar import build_sidebar, hide_sidebar_if_logged_out +from auth import get_fullname_for_user + +hide_sidebar_if_logged_out() + +st.set_page_config(page_title="Co-App Home", page_icon="🏠") + +authenticator = require_login() +st.session_state["authenticator"] = authenticator + +build_sidebar() + +username = st.session_state.get("username") +st.header("Controlling-Portal") +st.info(f"Willkommen, {get_fullname_for_user(username)}!") \ No newline at end of file diff --git a/app/ui/sidebar.py b/app/ui/sidebar.py new file mode 100644 index 0000000..5ad6f03 --- /dev/null +++ b/app/ui/sidebar.py @@ -0,0 +1,54 @@ +import streamlit as st +from auth import get_fullname_for_user, get_role_for_user + +def build_sidebar(): + + if st.session_state.get("authentication_status") != True: + return + + authenticator = st.session_state.get("authenticator") + username = st.session_state.get("username") + + if not authenticator or not username: + return + + role = get_role_for_user(username) + fullname = get_fullname_for_user(username) + + with st.sidebar: + st.logo("app/images/GMN_Logo_neu_rgb.png", size="small") + st.write(f"**{fullname}** ({role})") + + col1, col2 = st.columns(2) + with col1: + if st.button("Logout", use_container_width=True): + authenticator.logout("Logout", "unrendered") + st.rerun() + with col2: + if st.button("Home", use_container_width=True): + st.switch_page("pages/home.py") + st.divider() + st.markdown("## Menü") + + st.page_link("pages/Benutzer.py", label="Benutzer anlegen") + + + + + +# Damit die leere Sidebar komplett verschwindet nach dem Logout +def hide_sidebar_if_logged_out(): + if st.session_state.get("authentication_status") != True: + st.markdown(""" + + """, unsafe_allow_html=True) diff --git a/data/app.db b/data/app.db index 853d7b88588e0e3e1238759c0ec584aabcd2dcba..5382b6ff3c1865231507c8397fe197200dcc39c8 100644 GIT binary patch delta 1694 zcma)6Yitx%6rQ_xW*@V&_jE~dQ`WZI>=t%oTjsIT$6%zDL?Fb7P%Jd+cGw--h3#zF z?p8w-N{b0Tur4>zA12B#4Z+yR5J@m#VvIi#6XGK_#Gpn%NhHQ7CK~VVmS#)TILYLm znfu*yzVn@P?(_+M`UL+}Fl!UXuA3?nL9QS9rd|RA}5>F@^MMna3uX_u9Ng#AY5a_3F zKeLawb%+u6l4;>^$uVXm!~~T{&kBDl=VfVdGwo89(2<1mzQ>QK z%fA98oEttJ@qt@ts9f$4XJU)-z}f2-s}QFoJ&W)ZH(VYFITHCDt?~zsa_||v29HCG zTq94D4a6PzIB=A_sSkK10(CS??FIx$LZ3ceW;4?Qp6zX%+7BplXKFyKcaThw5D@8go&wY;)YI zGN{H*Rm55zU+FQF9?gq4YV@0~W@@!;LK1zqr897cpspLL?p$lRmq|c|Irttfz^kwy zHbE=-i!6}SWR?s&ZIS!Qp*Y@w{*MCwE^ICX{&%`!0uR;0|1C9~vwU=+Yo8!hh|rrY zF*>pKxbzH%$t(w6m?pD~XNep-?7|YKH60dK-q+mZ_0@m~9ne>h74L z#vUCl4sSlNWt+9x8p`#KnS;vEhMv-nU8Ms%2ev+F%cV_g)4R6z4UEgV^py2jNwqib z+FmKtcvsC&|LC~gm9ZYqn&~mq$~6vEy%!K85jreDdm!ZXQTo7OOffU#S-U@-Pni=F zRhu<1f6!F}29;0+_~QCOmm<^e)<;APS2_3%Zo*Y3*nb)PB5p#pkeJ?lBt!%_&%v*7 z9lnB#aGqgKK@bESMH7r%c5*VCw`vJ=OJe> z%WXG&4yVW!SVzvVQolixagR3`5#2Z6I*8ld-e9=;-7P~^kvxVq9aCpq{QK7o@A^Ey~z>_(N6isEun*tMc5 zVb|hjW8EgZwe)V5X&5bTE_T4ujnQo=$qFqD*E!aFx8ODl^);;V@CYGM#I?220UZTewX{yR& zJfw9KUL>>FHB&)lqqY`b9^@z7E-W@CHAZQJk;wFq4h>|p10%y(eP)*`8-{^rCBCM( zvDE1Kf*bDT!!&JQyrZ`uLbSiN#Lj%O5egN`K%=g?1u%0k_~J zgy|Mtq8Df<4+Wd3WGdoOlWjIvvwnXUcCclC)Dw@Iv0%Kl?O?1e*}}K{d&>gW+djYw z+=TPc1p)e-7P;Q>gtB7upyX%}doU~c!z^>)hD$^OokMgECN0JU^<2bmWE#_wQRg)j zwx>N6>^nO&Fl>j8Gb6oynatm4pIEh>$mhrDG!p)$#SJTMp*j=KVM|chEmhB3#_1=goIk9Nqv26cnR=A%Qg7hJhKo#IBoPt-7s#01j~BJ z-uVgvQg4yiLZO;HD=2PRkBB?qW+C7V39@e3)N_TZ3YYD)f#4^6g-=$SHY}$#V9!?x Qf45o_