Update migrations
This commit is contained in:
2
app/.streamlit/config.toml
Normal file
2
app/.streamlit/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[client]
|
||||||
|
showSidebarNavigation = false
|
||||||
17
app/auth.py
17
app/auth.py
@@ -1,4 +1,3 @@
|
|||||||
# app/auth_core.py
|
|
||||||
|
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
import bcrypt
|
import bcrypt
|
||||||
@@ -13,7 +12,7 @@ from db import get_conn #, create_user, verify_user, get_role_for_user
|
|||||||
def create_user(
|
def create_user(
|
||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
role: str = "user",
|
role_id: str = "user",
|
||||||
email: str | None = None,
|
email: str | None = None,
|
||||||
firstname: str | None = None,
|
firstname: str | None = None,
|
||||||
lastname: str | None = None
|
lastname: str | None = None
|
||||||
@@ -27,8 +26,8 @@ def create_user(
|
|||||||
try:
|
try:
|
||||||
with closing(get_conn()) as conn, conn:
|
with closing(get_conn()) as conn, conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO users (username, email, password_hash, role, firstname, lastname) VALUES (?, ?, ?, ?, ?, ?)",
|
"INSERT INTO users (username, email, password_hash, role_id, firstname, lastname) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
(username, email, pw_hash, role, firstname, lastname),
|
(username, email, pw_hash, role_id, firstname, lastname),
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -42,23 +41,23 @@ def create_user(
|
|||||||
def verify_user(username: str, password: str):
|
def verify_user(username: str, password: str):
|
||||||
"""
|
"""
|
||||||
Vergleicht eingegebenes Passwort mit dem gespeicherten bcrypt-Hash.
|
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:
|
with closing(get_conn()) as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT password_hash, role FROM users WHERE username = ?",
|
"SELECT password_hash, role_id FROM users WHERE username = ?",
|
||||||
(username,),
|
(username,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
stored_hash, role = row
|
stored_hash, role_id = row
|
||||||
|
|
||||||
# stored_hash ist TEXT → zurück nach bytes
|
# stored_hash ist TEXT → zurück nach bytes
|
||||||
ok = bcrypt.checkpw(password.encode("utf-8"), stored_hash.encode("utf-8"))
|
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:
|
def get_role_for_user(username: str) -> str:
|
||||||
with closing(get_conn()) as conn:
|
with closing(get_conn()) as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT role FROM users WHERE username = ?",
|
"SELECT role_id FROM users WHERE username = ?",
|
||||||
(username,),
|
(username,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return row[0] if row else "user"
|
return row[0] if row else "user"
|
||||||
|
|||||||
70
app/auth_runtime.py
Normal file
70
app/auth_runtime.py
Normal file
@@ -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
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
|
|
||||||
import streamlit as st
|
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):
|
def render(username: str, role: str):
|
||||||
st.title("Benutzerverwaltung")
|
st.title("Benutzerverwaltung")
|
||||||
|
|||||||
46
app/dashboards/home.py
Normal file
46
app/dashboards/home.py
Normal file
@@ -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}**.")
|
||||||
52
app/db.py
52
app/db.py
@@ -10,55 +10,3 @@ def get_conn():
|
|||||||
# check_same_thread=False, damit Streamlit mehrere Threads nutzen kann
|
# check_same_thread=False, damit Streamlit mehrere Threads nutzen kann
|
||||||
return sqlite3.connect(DB_PATH, check_same_thread=False)
|
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"
|
|
||||||
|
|||||||
486
app/main.py
486
app/main.py
@@ -1,186 +1,7 @@
|
|||||||
import importlib
|
|
||||||
|
|
||||||
import streamlit as st
|
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 version import __version__
|
||||||
from db import get_conn # an deine Struktur anpassen
|
from auth_runtime import require_login
|
||||||
|
from ui.sidebar import build_sidebar
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 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.<code> 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
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
@@ -189,303 +10,14 @@ def main():
|
|||||||
layout="centered",
|
layout="centered",
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Authenticator vorbereiten ---
|
authenticator = require_login()
|
||||||
with open("config/auth.yaml", "r", encoding="utf-8") as f:
|
# damit build_sidebar den authenticator findet:
|
||||||
base_config = yaml.load(f, Loader=SafeLoader)
|
st.session_state["authenticator"] = authenticator
|
||||||
|
|
||||||
db_creds = load_credentials_from_db()
|
build_sidebar()
|
||||||
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)
|
|
||||||
|
|
||||||
|
st.header("Controlling-Portal")
|
||||||
|
st.info(f"Willkommen, {st.session_state.get('username')}!")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
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()
|
|
||||||
141
app/main_old.py
141
app/main_old.py
@@ -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()
|
|
||||||
494
app/main_v1.py
Normal file
494
app/main_v1.py
Normal file
@@ -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.<code> 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()
|
||||||
43
app/pages/Benutzer.py
Normal file
43
app/pages/Benutzer.py
Normal file
@@ -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}**.")
|
||||||
17
app/pages/home.py
Normal file
17
app/pages/home.py
Normal file
@@ -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)}!")
|
||||||
54
app/ui/sidebar.py
Normal file
54
app/ui/sidebar.py
Normal file
@@ -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("""
|
||||||
|
<style>
|
||||||
|
/* komplette Sidebar + Toggle ausblenden */
|
||||||
|
[data-testid="stSidebar"] {display: none;}
|
||||||
|
[data-testid="stSidebarNav"] {display: none;}
|
||||||
|
[data-testid="collapsedControl"] {display: none;}
|
||||||
|
|
||||||
|
/* Content wieder ganz nach links */
|
||||||
|
[data-testid="stAppViewContainer"] {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
BIN
data/app.db
BIN
data/app.db
Binary file not shown.
@@ -29,11 +29,12 @@ create table if not exists roles (
|
|||||||
);
|
);
|
||||||
|
|
||||||
create table if not exists permissions (
|
create table if not exists permissions (
|
||||||
role_id integer unique not null,
|
role_id integer not null,
|
||||||
dash_id integer unique not null,
|
dash_id integer not null,
|
||||||
active integer not null default 1,
|
active integer not null default 1,
|
||||||
date_create TEXT NOT NULL DEFAULT (datetime('now')),
|
date_create TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
id integer primary key autoincrement
|
id integer primary key autoincrement,
|
||||||
|
unique (role_id, dash_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO schema_version (version) VALUES ('20251130_191100_add_table_dashboards');
|
INSERT INTO schema_version (version) VALUES ('20251130_191100_add_table_dashboards');
|
||||||
|
|||||||
8
migrations/20251203_201300_rename_col_table_role.sql
Normal file
8
migrations/20251203_201300_rename_col_table_role.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
rename column role to role_id;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251203_201300_rename_col_table_role');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column active integer default 1;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251203_202200_add_col_active_table_users');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
11
migrations/20251203_202900_add_col_order_tables.sql
Normal file
11
migrations/20251203_202900_add_col_order_tables.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table roles
|
||||||
|
add column order_no integer;
|
||||||
|
|
||||||
|
alter table permissions
|
||||||
|
add column order_no integer;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251203_202900_add_col_order_tables');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user