Compare commits
18 Commits
main
...
37d1daa052
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37d1daa052 | ||
|
|
9435399096 | ||
|
|
0143d9579b | ||
|
|
13d747827d | ||
|
|
e2091ec677 | ||
|
|
7f21716358 | ||
|
|
96be7567eb | ||
|
|
3c0e58dfc1 | ||
|
|
e2d96c712e | ||
|
|
ff7560b263 | ||
|
|
ed04a75ce2 | ||
|
|
5277830c50 | ||
|
|
111d0d2756 | ||
|
|
84e1e152f8 | ||
|
|
06e5322931 | ||
|
|
6158f2ddff | ||
|
|
fe33d714bd | ||
|
|
6f9c5eb34e |
154
README.md
154
README.md
@@ -1,3 +1,155 @@
|
|||||||
# co_app
|
# co_app
|
||||||
|
|
||||||
Controlling-App
|
## Insallation
|
||||||
|
|
||||||
|
1. Git-Repository clonen
|
||||||
|
2.
|
||||||
|
|
||||||
|
## Entwicklungs- und Produktionsumgebung steuern (APP_ENV)
|
||||||
|
Die App unterscheidet zwischen Entwicklung und Produktion über die Umgebungsvariable APP_ENV.
|
||||||
|
|
||||||
|
### Entwicklung (lokal)
|
||||||
|
Im Terminal setzen:
|
||||||
|
```bash
|
||||||
|
export APP_ENV=dev
|
||||||
|
```
|
||||||
|
Die Variable gilt, solange das Terminal geöffnet ist.
|
||||||
|
Beim Starten der App:
|
||||||
|
```bash
|
||||||
|
streamlit run app/main.py
|
||||||
|
```
|
||||||
|
Die App erkennt automatisch den Dev-Modus.
|
||||||
|
|
||||||
|
### Produktion (Systemd-Dienst)
|
||||||
|
|
||||||
|
Für den produktiven Betrieb setzt systemd die Variable dauerhaft auf prod:
|
||||||
|
```ini
|
||||||
|
Environment=APP_ENV=prod
|
||||||
|
```
|
||||||
|
In prod-Modus laufen z. B. Logging und Pfade anders als lokal.
|
||||||
|
|
||||||
|
### systemd-Dienst einrichten
|
||||||
|
``/etc/systemd/system/myapp.service``
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=MyApp Streamlit Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=co_app
|
||||||
|
Group=co_app
|
||||||
|
WorkingDirectory=/opt/co_app
|
||||||
|
Environment=APP_ENV=prod
|
||||||
|
ExecStart=/usr/bin/python3 -m streamlit run app/main.py --server.port=8501
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
**Installation & Start**
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable co_app.service
|
||||||
|
sudo systemctl start co_app.service
|
||||||
|
```
|
||||||
|
**Logs ansehen**
|
||||||
|
Da streamlit + python ins systemd-Journal loggen:
|
||||||
|
```bash
|
||||||
|
journalctl -u co_app.service -f
|
||||||
|
```
|
||||||
|
**Wichtig**
|
||||||
|
- APP_ENV=dev → Entwicklung, lokale Logs, lokale DB
|
||||||
|
- APP_ENV=prod → Systemd-Betrieb, serverseitige Pfade, Logging ins Journal
|
||||||
|
- Die App liest die Variable mit:
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
APP_ENV = os.environ.get("APP_ENV", "dev")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Setup & Migration Guide
|
||||||
|
|
||||||
|
Dieses Projekt verwendet SQLite für die interne App-Datenbank.
|
||||||
|
Das Schema wird vollständig über SQL-Migrationsdateien verwaltet, die automatisch in der richtigen Reihenfolge ausgeführt werden.
|
||||||
|
|
||||||
|
**WICHTIG:**
|
||||||
|
|
||||||
|
Bei der ersten Initialisierung muss ein admin-Passwort vergeben werden. Beim erstmaligen Anmelden an der App muss man sich dann als "adim" mit dem vergebenen Passwort anmelden. In der App können dann User angelegt.
|
||||||
|
|
||||||
|
### Ordnerstruktur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
data/ ← enthält die erzeugte SQLite-Datenbank (app.db)
|
||||||
|
migrations/ ← enthält alle SQL-Migrationsdateien
|
||||||
|
migrate.py ← Python-Skript zum Anwenden der Migrationen
|
||||||
|
```
|
||||||
|
### Erstellen der Datenbank (Initial Setup)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python migrate.py
|
||||||
|
```
|
||||||
|
Das Skript:
|
||||||
|
|
||||||
|
1. erstellt den Ordner db/ (falls nicht vorhanden)
|
||||||
|
2. erzeugt eine neue Datei db/app.db
|
||||||
|
3. führt alle SQL-Dateien im Ordner migrations/ der Reihe nach aus
|
||||||
|
4. protokolliert die angewendeten Migrationen in der Tabelle schema_version
|
||||||
|
|
||||||
|
Danach ist die Datenbank vollständig initialisiert und einsatzbereit.
|
||||||
|
|
||||||
|
### Arbeiten mit Migrationen (Änderungen am Schema)
|
||||||
|
|
||||||
|
Änderungen an der Datenbankstruktur werden nie direkt an der DB vorgenommen.
|
||||||
|
Stattdessen erzeugst du eine neue Migrationsdatei im Ordner migrations/.
|
||||||
|
|
||||||
|
**Beispiel: neue Spalte hinzufügen**
|
||||||
|
|
||||||
|
Lege eine Datei an
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch /migrations/20250301_101500_add_last_login.sql
|
||||||
|
```
|
||||||
|
Dateiinhalt
|
||||||
|
```sql
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN last_login DATETIME;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version)
|
||||||
|
VALUES ('20250301_101500_add_last_login');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
```
|
||||||
|
Migration anwenden
|
||||||
|
```bash
|
||||||
|
python migrate.py
|
||||||
|
```
|
||||||
|
Das Skript erkennt automatisch, dass diese Migration neu ist und führt sie aus.
|
||||||
|
|
||||||
|
### Erneutes Ausführen (Updates)
|
||||||
|
Wenn eine bestehende Installation aktualisiert werden soll:
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
python migrate.py
|
||||||
|
```
|
||||||
|
Das Skript:
|
||||||
|
|
||||||
|
- liest die Tabelle schema_version
|
||||||
|
- führt nur Migrationen aus, die noch nicht angewendet wurden
|
||||||
|
|
||||||
|
Bestehende Strukturen bleiben unberührt.
|
||||||
|
|
||||||
|
### Entwicklungsphase: Hinweise
|
||||||
|
|
||||||
|
Solange das Projekt noch nicht produktiv genutzt wird, gilt:
|
||||||
|
- du kannst Migrationsdateien löschen/zusammenführen
|
||||||
|
- du kannst die komplette app.db jederzeit löschen
|
||||||
|
- vor dem ersten echten Deploy kannst du alle Migrationen zu einer sauberen initial.sql zusammenfassen
|
||||||
|
|
||||||
|
👉 **Nach dem ersten produktiven Einsatz** werden Migrationen nie mehr gelöscht, sondern nur ergänzt.
|
||||||
|
|
||||||
|
### Optional: DB neu erzeugen für Tests
|
||||||
|
Um eine frische Datenbank zu bekommen:
|
||||||
|
```bash
|
||||||
|
rm -f data/app.db
|
||||||
|
python migrate.py
|
||||||
|
```
|
||||||
|
|||||||
10
app/.streamlit/config.toml
Normal file
10
app/.streamlit/config.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[client]
|
||||||
|
showSidebarNavigation = false
|
||||||
|
toolbarMode = "minimal"
|
||||||
|
# toolbarMode = "auto"
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
# primaryColor = "blue"
|
||||||
|
# backgroundColor = "black"
|
||||||
|
# secondaryBackgroundColor = "blue"
|
||||||
|
# borderColor = "blue"
|
||||||
21
app/.streamlit/style.css
Normal file
21
app/.streamlit/style.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
|
||||||
|
/* Footer entfernen */
|
||||||
|
footer {visibility: hidden !important;}
|
||||||
|
div[data-testid="stStatusWidget"] {display: none !important;}
|
||||||
|
|
||||||
|
/* Sidebar-Spacing kompakter */
|
||||||
|
section[data-testid="stSidebar"] > div {
|
||||||
|
padding-top: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Haupt-Container kompakter */
|
||||||
|
.block-container {
|
||||||
|
padding-top: 0.6rem !important;
|
||||||
|
padding-bottom: 0.6rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Widgets enger */
|
||||||
|
.stButton, .stTextInput, .stSelectbox {
|
||||||
|
margin-bottom: 0.3rem !important;
|
||||||
|
}
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
86
app/admin_users.py
Normal file
86
app/admin_users.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# admin_users.py
|
||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
from db_users import init_db, list_users, get_user, create_user, update_user, set_password, delete_user
|
||||||
|
|
||||||
|
st.set_page_config(page_title="User Admin", layout="wide")
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
st.title("Benutzerverwaltung")
|
||||||
|
|
||||||
|
# --- Übersicht ---
|
||||||
|
users = list_users()
|
||||||
|
df = pd.DataFrame(users)
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
st.info("Noch keine Benutzer vorhanden.")
|
||||||
|
df = pd.DataFrame(columns=["id","username","display_name","email","role","is_active","created_at"])
|
||||||
|
|
||||||
|
st.subheader("Übersicht")
|
||||||
|
st.dataframe(df, use_container_width=True, hide_index=True)
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
colA, colB = st.columns(2, gap="large")
|
||||||
|
|
||||||
|
# --- Neuen Benutzer anlegen ---
|
||||||
|
with colA:
|
||||||
|
st.subheader("Neuen Benutzer anlegen")
|
||||||
|
with st.form("create_user", clear_on_submit=True):
|
||||||
|
username = st.text_input("Username*", max_chars=50)
|
||||||
|
password = st.text_input("Initiales Passwort*", type="password")
|
||||||
|
display_name = st.text_input("Anzeigename")
|
||||||
|
email = st.text_input("E-Mail")
|
||||||
|
role = st.selectbox("Rolle", ["user", "admin"], index=0)
|
||||||
|
is_active = st.checkbox("Aktiv", value=True)
|
||||||
|
submitted = st.form_submit_button("Anlegen")
|
||||||
|
|
||||||
|
if submitted:
|
||||||
|
if not username or not password:
|
||||||
|
st.error("Username und Passwort sind Pflicht.")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
create_user(username, password, display_name, email, role, is_active)
|
||||||
|
st.success("Benutzer angelegt.")
|
||||||
|
st.rerun()
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Fehler beim Anlegen: {e}")
|
||||||
|
|
||||||
|
# --- Bestehenden Benutzer bearbeiten/löschen ---
|
||||||
|
with colB:
|
||||||
|
st.subheader("Benutzer bearbeiten / löschen")
|
||||||
|
|
||||||
|
user_ids = df["id"].tolist() if "id" in df.columns else []
|
||||||
|
selected_id = st.selectbox("Benutzer wählen", user_ids, format_func=lambda uid: f"{uid} – {df.loc[df.id==uid,'username'].values[0] if len(df)>0 else uid}" if uid in user_ids else str(uid))
|
||||||
|
|
||||||
|
user = get_user(int(selected_id)) if selected_id else None
|
||||||
|
if user:
|
||||||
|
with st.form("edit_user"):
|
||||||
|
display_name2 = st.text_input("Anzeigename", value=user.get("display_name") or "")
|
||||||
|
email2 = st.text_input("E-Mail", value=user.get("email") or "")
|
||||||
|
role2 = st.selectbox("Rolle", ["user", "admin"], index=0 if user["role"]=="user" else 1)
|
||||||
|
is_active2 = st.checkbox("Aktiv", value=bool(user["is_active"]))
|
||||||
|
save = st.form_submit_button("Änderungen speichern")
|
||||||
|
|
||||||
|
if save:
|
||||||
|
update_user(int(selected_id), display_name2, email2, role2, is_active2)
|
||||||
|
st.success("Gespeichert.")
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
st.markdown("**Passwort zurücksetzen**")
|
||||||
|
with st.form("reset_pw", clear_on_submit=True):
|
||||||
|
new_pw = st.text_input("Neues Passwort", type="password")
|
||||||
|
reset = st.form_submit_button("Passwort setzen")
|
||||||
|
if reset:
|
||||||
|
if not new_pw:
|
||||||
|
st.error("Bitte ein Passwort eingeben.")
|
||||||
|
else:
|
||||||
|
set_password(int(selected_id), new_pw)
|
||||||
|
st.success("Passwort aktualisiert.")
|
||||||
|
|
||||||
|
st.markdown("**Löschen**")
|
||||||
|
confirm = st.checkbox("Ich möchte diesen Benutzer wirklich löschen.")
|
||||||
|
if st.button("Benutzer löschen", disabled=not confirm):
|
||||||
|
delete_user(int(selected_id))
|
||||||
|
st.success("Benutzer gelöscht.")
|
||||||
|
st.rerun()
|
||||||
BIN
app/app_db/app.db
Normal file
BIN
app/app_db/app.db
Normal file
Binary file not shown.
45
app/app_db/app_db.py
Normal file
45
app/app_db/app_db.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
# DB_PATH = BASE_DIR / "app_db" / "app.db"
|
||||||
|
DB_PATH = BASE_DIR / "app.db"
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------------
|
||||||
|
# Allgemeine Datenbankfunktionen
|
||||||
|
#-------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_conn():
|
||||||
|
# check_same_thread=False, damit Streamlit mehrere Threads nutzen kann
|
||||||
|
return sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
|
|
||||||
|
def get_list(sql, params=None):
|
||||||
|
conn = get_conn()
|
||||||
|
df = pd.read_sql_query(sql, conn, params=params)
|
||||||
|
conn.close()
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_value(sql, params=None):
|
||||||
|
conn = get_conn()
|
||||||
|
df = pd.read_sql_query(sql, conn, params=params)
|
||||||
|
conn.close()
|
||||||
|
value = df.iloc[0,0]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def send_cmd(sql, params=None):
|
||||||
|
try:
|
||||||
|
with get_conn() as conn:
|
||||||
|
if params is None:
|
||||||
|
conn.execute(sql)
|
||||||
|
else:
|
||||||
|
conn.execute(sql, params)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print("DB-Fehler:", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------------------
|
||||||
224
app/auth.py
Normal file
224
app/auth.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
|
||||||
|
from contextlib import closing
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
from app_db.app_db import get_conn, get_list
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Benutzer anlegen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def create_user(
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
role_id: int = 1,
|
||||||
|
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_id, firstname, lastname) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
(username, email, pw_hash, role_id, firstname, lastname),
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Benutzer überprüfen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def verify_user(username: str, password: str):
|
||||||
|
"""
|
||||||
|
Vergleicht eingegebenes Passwort mit dem gespeicherten bcrypt-Hash.
|
||||||
|
Rückgabe: (True, role_id) oder (False, None)
|
||||||
|
"""
|
||||||
|
with closing(get_conn()) as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT password_hash, role_id FROM users WHERE username = ?",
|
||||||
|
(username,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
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_id) if ok else (False, None)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Rolle für einen Benutzer holen (für Streamlit-Inhalte)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_role_for_user(username: str) -> str:
|
||||||
|
with closing(get_conn()) as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT r.role_text from users u left join roles r on u.role_id = r.role_id WHERE username = ?",
|
||||||
|
(username,),
|
||||||
|
).fetchone()
|
||||||
|
return row[0] if row else "user"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fullname für einen Benutzer holen (für Streamlit-Inhalte)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_fullname_for_user(username: str) -> str:
|
||||||
|
with closing(get_conn()) as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT firstname ||' '|| lastname as fullname FROM users WHERE username = ?",
|
||||||
|
(username,),
|
||||||
|
).fetchone()
|
||||||
|
return row[0] if row else "user"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sidebar-Einträge für den Benutzer
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_sidebar(role_text: str, username: str):
|
||||||
|
if role_text == "admin":
|
||||||
|
sql = """
|
||||||
|
select
|
||||||
|
g.group_id,
|
||||||
|
g.group_text,
|
||||||
|
d.dash_id,
|
||||||
|
d.dash_text,
|
||||||
|
d.page_file,
|
||||||
|
d.dash_type,
|
||||||
|
d.order_no as dash_order,
|
||||||
|
g.order_no as group_order
|
||||||
|
from
|
||||||
|
groups g
|
||||||
|
left join dashboards d
|
||||||
|
on g.group_id = d.group_id
|
||||||
|
where
|
||||||
|
g.active = 1
|
||||||
|
and d.active = 1
|
||||||
|
order by g.order_no, d.order_no
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
sql = """
|
||||||
|
SELECT
|
||||||
|
d.group_id,
|
||||||
|
g.group_text,
|
||||||
|
p.dash_id,
|
||||||
|
d.dash_text,
|
||||||
|
d.page_file,
|
||||||
|
d.dash_type,
|
||||||
|
d.order_no as dash_order,
|
||||||
|
g.order_no as group_order
|
||||||
|
FROM
|
||||||
|
users u
|
||||||
|
left join permissions p
|
||||||
|
on u.role_id = p.role_id
|
||||||
|
left join dashboards d
|
||||||
|
on p.dash_id = d.dash_id
|
||||||
|
left join groups g
|
||||||
|
on d.group_id = g.group_id
|
||||||
|
where
|
||||||
|
u.active = 1
|
||||||
|
and g.active = 1
|
||||||
|
and d.active = 1
|
||||||
|
and p.active = 1
|
||||||
|
and u.username = ?
|
||||||
|
order by g.order_no, d.order_no
|
||||||
|
"""
|
||||||
|
params = (username,) if "?" in sql else None
|
||||||
|
df = get_list(sql, params)
|
||||||
|
return df
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Credential-Lader für streamlit-authenticator
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def load_credentials_from_db() -> dict:
|
||||||
|
"""
|
||||||
|
Baut das credentials-Dict so:
|
||||||
|
{
|
||||||
|
"usernames": {
|
||||||
|
"hansi": {
|
||||||
|
"email": "hansi@example.com",
|
||||||
|
"name": "hansi",
|
||||||
|
"password": "$2b$12$...",
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streamlit-authenticator prüft dann Login + Cookies automatisch.
|
||||||
|
"""
|
||||||
|
creds = {"usernames": {}}
|
||||||
|
|
||||||
|
with closing(get_conn()) as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT username, email, password_hash FROM users"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
for username, email, pw_hash in rows:
|
||||||
|
# pw_hash kommt als TEXT
|
||||||
|
creds["usernames"][username] = {
|
||||||
|
"name": username,
|
||||||
|
"email": email or f"{username}@example.local",
|
||||||
|
"password": pw_hash, # TEXT → streamlit-authenticator-kompatibel
|
||||||
|
}
|
||||||
|
|
||||||
|
return creds
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Muss das Passwort geändert werden (Admin-Vorgabe)?
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def needs_password_change(username: str) -> bool:
|
||||||
|
"""Prüft, ob der User ein neues Passwort setzen muss (new_pwd = 1)."""
|
||||||
|
with closing(get_conn()) as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT new_pwd FROM users WHERE username = ?",
|
||||||
|
(username,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return False
|
||||||
|
return row[0] == 1
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Passwort ändern durch den Benutzer
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def update_password(username: str, new_password: str, reset_flag: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
Setzt ein neues Passwort für den User.
|
||||||
|
Wenn reset_flag=True: new_pwd wird auf 0 zurückgesetzt.
|
||||||
|
"""
|
||||||
|
pw_hash = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||||
|
|
||||||
|
with closing(get_conn()) as conn, conn:
|
||||||
|
if reset_flag:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = ?, new_pwd = 0
|
||||||
|
WHERE username = ?
|
||||||
|
""",
|
||||||
|
(pw_hash, username),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = ?
|
||||||
|
WHERE username = ?
|
||||||
|
""",
|
||||||
|
(pw_hash, username),
|
||||||
|
)
|
||||||
|
return True
|
||||||
108
app/auth_runtime.py
Normal file
108
app/auth_runtime.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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,
|
||||||
|
get_role_for_user,
|
||||||
|
get_fullname_for_user,
|
||||||
|
get_sidebar
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
/* Streamlit Hamburger-Menü ausblenden */
|
||||||
|
div[data-testid="stToolbar"] {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: ganz entfernen statt nur unsichtbar machen */
|
||||||
|
div[data-testid="stDecoration"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
|
||||||
|
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() # Hier wird name, email und password geladen
|
||||||
|
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()
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------------------------
|
||||||
|
# Ab hier bin ich eingeloggt
|
||||||
|
#--------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
username = st.session_state.get("username")
|
||||||
|
|
||||||
|
if "df_sidebar" not in st.session_state:
|
||||||
|
role_text = get_role_for_user(username)
|
||||||
|
fullname = get_fullname_for_user(username)
|
||||||
|
sidebar = get_sidebar(role_text, username)
|
||||||
|
|
||||||
|
st.session_state["role_text"] = role_text
|
||||||
|
st.session_state["fullname"] = fullname
|
||||||
|
st.session_state["df_sidebar"] = sidebar
|
||||||
|
|
||||||
|
# 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.")
|
||||||
|
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
|
||||||
0
app/data/__init__.py
Normal file
0
app/data/__init__.py
Normal file
36
app/data/db.py
Normal file
36
app/data/db.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from sqlalchemy import create_engine, Text
|
||||||
|
import pandas as pd
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import quote
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
env_path = Path("config/settings.env")
|
||||||
|
load_dotenv(env_path)
|
||||||
|
|
||||||
|
oracle_conn_str = os.getenv("oracle_conn_str")
|
||||||
|
co_dw_conn_str = os.getenv("co_dw_conn_str")
|
||||||
|
co_daten_conn_str = os.getenv("co_daten_conn_str")
|
||||||
|
|
||||||
|
def get_conn(db):
|
||||||
|
match db:
|
||||||
|
case "oracle":
|
||||||
|
engine = create_engine(oracle_conn_str)
|
||||||
|
case "co_dw":
|
||||||
|
engine = create_engine(co_dw_conn_str)
|
||||||
|
case "co_daten":
|
||||||
|
engine = create_engine(co_dw_conn_str)
|
||||||
|
case _:
|
||||||
|
logging.info(f"Datenbank {db} konnte nicht gefunden werden")
|
||||||
|
return engine
|
||||||
|
|
||||||
|
# def get_data(db):
|
||||||
|
# engine = get_conn(db)
|
||||||
|
# with engine.connect() as conn:
|
||||||
|
# print(engine)
|
||||||
|
# return
|
||||||
|
|
||||||
|
# if __name__ == "__main__":
|
||||||
|
# get_data("co_daten")
|
||||||
10
app/data/scriptloader.py
Normal file
10
app/data/scriptloader.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def get_sql(filename):
|
||||||
|
sql_query = Path(f"app/data/sql/{filename}.sql").read_text()
|
||||||
|
return sql_query
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(get_sql("sales_umsatz"))
|
||||||
1
app/data/sql/co_kostenobjekte.sql
Normal file
1
app/data/sql/co_kostenobjekte.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
select * from bi.Dim_Kostenobjekt
|
||||||
1
app/data/sql/sales_umsatz.sql
Normal file
1
app/data/sql/sales_umsatz.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
select * from umsatz
|
||||||
73
app/db_users.py
Normal file
73
app/db_users.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# db_users.py
|
||||||
|
import sqlite3
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Optional
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
DB_PATH = "app.db"
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_conn():
|
||||||
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
display_name TEXT,
|
||||||
|
email TEXT,
|
||||||
|
password_hash BLOB NOT NULL,
|
||||||
|
role TEXT NOT NULL DEFAULT 'user',
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
def list_users():
|
||||||
|
with get_conn() as conn:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT id, username, display_name, email, role, is_active, created_at
|
||||||
|
FROM users ORDER BY id
|
||||||
|
""").fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
def get_user(user_id: int) -> Optional[dict]:
|
||||||
|
with get_conn() as conn:
|
||||||
|
r = conn.execute("""
|
||||||
|
SELECT id, username, display_name, email, role, is_active
|
||||||
|
FROM users WHERE id = ?
|
||||||
|
""", (user_id,)).fetchone()
|
||||||
|
return dict(r) if r else None
|
||||||
|
|
||||||
|
def create_user(username: str, password: str, display_name: str = "", email: str = "", role: str = "user", is_active: bool = True):
|
||||||
|
pw_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO users (username, display_name, email, password_hash, role, is_active)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""", (username, display_name, email, pw_hash, role, 1 if is_active else 0))
|
||||||
|
|
||||||
|
def update_user(user_id: int, display_name: str, email: str, role: str, is_active: bool):
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET display_name=?, email=?, role=?, is_active=?
|
||||||
|
WHERE id=?
|
||||||
|
""", (display_name, email, role, 1 if is_active else 0, user_id))
|
||||||
|
|
||||||
|
def set_password(user_id: int, new_password: str):
|
||||||
|
pw_hash = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt())
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("UPDATE users SET password_hash=? WHERE id=?", (pw_hash, user_id))
|
||||||
|
|
||||||
|
def delete_user(user_id: int):
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("DELETE FROM users WHERE id=?", (user_id,))
|
||||||
41
app/db_users_search.py
Normal file
41
app/db_users_search.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# db_users_search.py
|
||||||
|
import sqlite3
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
DB_PATH = "app.db"
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_conn():
|
||||||
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def search_users(q: str, limit: int, offset: int):
|
||||||
|
q = (q or "").strip()
|
||||||
|
like = f"%{q}%"
|
||||||
|
with get_conn() as conn:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT id, username, display_name, email, role, is_active, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE (? = '')
|
||||||
|
OR (username LIKE ? COLLATE NOCASE
|
||||||
|
OR display_name LIKE ? COLLATE NOCASE
|
||||||
|
OR email LIKE ? COLLATE NOCASE)
|
||||||
|
ORDER BY username
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""", (q, like, like, like, limit, offset)).fetchall()
|
||||||
|
|
||||||
|
total = conn.execute("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM users
|
||||||
|
WHERE (? = '')
|
||||||
|
OR (username LIKE ? COLLATE NOCASE
|
||||||
|
OR display_name LIKE ? COLLATE NOCASE
|
||||||
|
OR email LIKE ? COLLATE NOCASE)
|
||||||
|
""", (q, like, like, like)).fetchone()[0]
|
||||||
|
|
||||||
|
return [dict(r) for r in rows], int(total)
|
||||||
BIN
app/images/GMN_Logo_neu_rgb.png
Normal file
BIN
app/images/GMN_Logo_neu_rgb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
app/images/home.png
Normal file
BIN
app/images/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 308 B |
24
app/logging_config.py
Normal file
24
app/logging_config.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def setup_logging(app_env: str):
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
||||||
|
|
||||||
|
# Always log to console (terminal / systemd)
|
||||||
|
sh = logging.StreamHandler()
|
||||||
|
sh.setFormatter(fmt)
|
||||||
|
logger.addHandler(sh)
|
||||||
|
|
||||||
|
# Dev → additional file logging
|
||||||
|
if app_env == "dev":
|
||||||
|
log_dir = Path("logs")
|
||||||
|
log_dir.mkdir(exist_ok=True)
|
||||||
|
fh = logging.FileHandler(log_dir / "app.log", encoding="utf-8")
|
||||||
|
fh.setFormatter(fmt)
|
||||||
|
logger.addHandler(fh)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
61
app/main.py
Normal file
61
app/main.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import streamlit as st
|
||||||
|
import logging
|
||||||
|
from logging_config import setup_logging
|
||||||
|
from version import __version__
|
||||||
|
from auth import get_role_for_user, get_fullname_for_user, get_sidebar
|
||||||
|
from auth_runtime import require_login
|
||||||
|
from ui.sidebar import build_sidebar
|
||||||
|
import os
|
||||||
|
from app_db.app_db import get_list
|
||||||
|
from pathlib import Path
|
||||||
|
from tools.load_css import load_css
|
||||||
|
|
||||||
|
|
||||||
|
APP_ENV = os.environ.get("APP_ENV", "dev")
|
||||||
|
|
||||||
|
logger = setup_logging(APP_ENV)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
load_css()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
st.set_page_config(
|
||||||
|
page_title=f"Co-App Home - V{__version__}",
|
||||||
|
page_icon="🔒",
|
||||||
|
layout="centered",
|
||||||
|
menu_items=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
authenticator = require_login()
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------------------
|
||||||
|
# Ab hier ist Benutzer angemeldet!!
|
||||||
|
#--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Alles was man so braucht in der App wird in session_state abgelegt
|
||||||
|
|
||||||
|
# zuerst den authenticator holen ...
|
||||||
|
st.session_state["authenticator"] = authenticator
|
||||||
|
# ... dann ist der name des users in der sesstion_state verfügbar!
|
||||||
|
|
||||||
|
# Ich suche mir erst mal alles zusammen, was ich dann in session_state speichern will ...
|
||||||
|
username = st.session_state.get("name") # wird duch authenticator gesetzt
|
||||||
|
role_text = get_role_for_user(username)
|
||||||
|
fullname = get_fullname_for_user(username)
|
||||||
|
sidebar = get_sidebar(role_text, username)
|
||||||
|
|
||||||
|
# ... und lege es dann im session_state ab.
|
||||||
|
st.session_state["role_text"] = role_text
|
||||||
|
st.session_state["fullname"] = fullname
|
||||||
|
st.session_state["df_sidebar"] = sidebar
|
||||||
|
|
||||||
|
build_sidebar()
|
||||||
|
|
||||||
|
st.header("Controlling-Portal")
|
||||||
|
st.info(f"Willkommen, {st.session_state.get('username')}!")
|
||||||
|
|
||||||
|
st.text("Hier könnte eine Nachricht für den Benutzer stehen")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
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()
|
||||||
133
app/migrate.py
Normal file
133
app/migrate.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import sqlite3
|
||||||
|
from version import __version__
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
from logging_config import setup_logging
|
||||||
|
import os
|
||||||
|
from contextlib import closing
|
||||||
|
import getpass
|
||||||
|
from auth import create_user
|
||||||
|
|
||||||
|
APP_ENV = os.environ.get("APP_ENV", "dev")
|
||||||
|
logger = setup_logging(APP_ENV)
|
||||||
|
logger.info(f"Starting migration - APP-Version {__version__}")
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
DB_DIR = BASE_DIR / "app" / "app_db"
|
||||||
|
DB_PATH = DB_DIR / "app.db"
|
||||||
|
MIGRATIONS_DIR = BASE_DIR / "migrations"
|
||||||
|
ADMIN_USERNAME = "admin"
|
||||||
|
|
||||||
|
print(BASE_DIR)
|
||||||
|
|
||||||
|
def get_connection() -> sqlite3.Connection:
|
||||||
|
DB_DIR.mkdir(exist_ok=True)
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def get_applied_versions(conn: sqlite3.Connection) -> set[str]:
|
||||||
|
"""
|
||||||
|
Ließt aus schema_version, welche Migrationen schon gelaufen sind.
|
||||||
|
Falls die Tabelle noch nicht existiert → leeres Set.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cur = conn.execute("SELECT version FROM schema_version")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return {r[0] for r in rows}
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
# Tabelle existiert noch nicht (z.B. bei initialer DB)
|
||||||
|
|
||||||
|
return set()
|
||||||
|
|
||||||
|
def apply_migrations():
|
||||||
|
if not MIGRATIONS_DIR.exists():
|
||||||
|
raise SystemExit(f"Migrations-Ordner nicht gefunden: {MIGRATIONS_DIR}")
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
applied = get_applied_versions(conn)
|
||||||
|
|
||||||
|
# Alle .sql-Dateien alphabetisch sortiert
|
||||||
|
sql_files = sorted(MIGRATIONS_DIR.glob("*.sql"))
|
||||||
|
|
||||||
|
if not sql_files:
|
||||||
|
logger.info("Keine Migrationsdateien gefunden.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for path in sql_files:
|
||||||
|
version = path.stem # z.B. '20250220_120000_initial'
|
||||||
|
|
||||||
|
if version in applied:
|
||||||
|
logger.info(f"[SKIP] {version} (bereits angewendet)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"[APPLY] {version}")
|
||||||
|
sql = path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Eine Transaktion pro Datei
|
||||||
|
with conn:
|
||||||
|
conn.executescript(sql)
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Fehler in Migration {version}: {e}")
|
||||||
|
# hier nicht weiter machen
|
||||||
|
raise
|
||||||
|
logger.info(f"Migrationen abgeschlossen. DB: {DB_PATH}")
|
||||||
|
|
||||||
|
create_admin_user()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def admin_exists() -> bool:
|
||||||
|
"""Prüft, ob der Admin-User bereits existiert."""
|
||||||
|
with closing(get_connection()) as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT 1 FROM users WHERE username = ?",
|
||||||
|
(ADMIN_USERNAME,),
|
||||||
|
).fetchone()
|
||||||
|
if row is not None:
|
||||||
|
print(row[0])
|
||||||
|
else:
|
||||||
|
print(ADMIN_USERNAME)
|
||||||
|
print("Kein Admin gefunden")
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
def create_admin_user():
|
||||||
|
if admin_exists():
|
||||||
|
logger.info("Adminkonto existiert bereits! Kein initiales Konto angelegt.")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Adminkonto wird angelegt ...")
|
||||||
|
|
||||||
|
pw1 = getpass.getpass("Passwort: ")
|
||||||
|
pw2 = getpass.getpass("Passwort wiederholen: ")
|
||||||
|
|
||||||
|
if pw1 != pw2:
|
||||||
|
logger.warning("Passwörter stimmen nicht überein! Abbruch.")
|
||||||
|
return
|
||||||
|
|
||||||
|
ok = create_user(
|
||||||
|
username=ADMIN_USERNAME,
|
||||||
|
password=pw1,
|
||||||
|
role_id=1,
|
||||||
|
email="admin@co_app",
|
||||||
|
firstname="***",
|
||||||
|
lastname="admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
logger.info(f"Admin-Benutzer '{ADMIN_USERNAME}' wurde angelegt.")
|
||||||
|
else:
|
||||||
|
# Sollte eigentlich nicht passieren, weil wir vorher geprüft haben,
|
||||||
|
# aber falls z.B. Parallelzugriff o.Ä.
|
||||||
|
logger.info(f"Admin-Benutzer '{ADMIN_USERNAME}' konnte nicht angelegt werden.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
apply_migrations()
|
||||||
0
app/pages/__init__.py
Normal file
0
app/pages/__init__.py
Normal file
34
app/pages/costobjects.py
Normal file
34
app/pages/costobjects.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
from data.scriptloader import get_sql
|
||||||
|
from data.db import get_conn
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def load_data():
|
||||||
|
sql = get_sql("co_kostenobjekte")
|
||||||
|
print(sql)
|
||||||
|
engine = get_conn("co_dw")
|
||||||
|
with engine.connect() as conn:
|
||||||
|
df = pd.read_sql(sql, engine)
|
||||||
|
print(df)
|
||||||
|
return df
|
||||||
|
|
||||||
|
st.dataframe(load_data())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
df = load_data()
|
||||||
|
print(df)
|
||||||
124
app/pages/groups.py
Normal file
124
app/pages/groups.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import streamlit as st
|
||||||
|
from auth_runtime import require_login
|
||||||
|
# from ui.sidebar import build_sidebar
|
||||||
|
from pathlib import Path
|
||||||
|
from tools.load_css import load_css
|
||||||
|
from app_db.app_db import get_list, send_cmd
|
||||||
|
from tools.numgen import get_num
|
||||||
|
|
||||||
|
|
||||||
|
DASH_NAME = Path(__file__).stem
|
||||||
|
|
||||||
|
load_css()
|
||||||
|
|
||||||
|
st.set_page_config(page_title="Co-App Benutzer", page_icon=":material/person:", layout="wide", initial_sidebar_state="collapsed")
|
||||||
|
|
||||||
|
authenticator = require_login()
|
||||||
|
st.session_state["authenticator"] = authenticator
|
||||||
|
|
||||||
|
def sidebar():
|
||||||
|
|
||||||
|
fullname = st.session_state.get("fullname")
|
||||||
|
role_text = st.session_state.get("role_text")
|
||||||
|
|
||||||
|
with st.sidebar:
|
||||||
|
|
||||||
|
st.logo("app/images/GMN_Logo_neu_rgb.png", size="small")
|
||||||
|
st.markdown(f"**{fullname}** ({role_text})")
|
||||||
|
|
||||||
|
col1, col2 = st.columns([2,2])
|
||||||
|
with col1:
|
||||||
|
authenticator.logout("Logout")
|
||||||
|
with col2:
|
||||||
|
if st.button("Home", use_container_width=True, icon=":material/home:"):
|
||||||
|
st.switch_page("pages/home.py")
|
||||||
|
|
||||||
|
groups()
|
||||||
|
|
||||||
|
|
||||||
|
def groups():
|
||||||
|
|
||||||
|
if "selected_user_id" not in st.session_state:
|
||||||
|
st.session_state.selected_user_id = None
|
||||||
|
if st.button(label="test"):
|
||||||
|
st.info(get_num("numgen_group", step=10))
|
||||||
|
#--------------------------------------------------------------------------------------------------
|
||||||
|
# Rollenverwaltung
|
||||||
|
#--------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
# df = get_list("""
|
||||||
|
# select
|
||||||
|
# u.id,
|
||||||
|
# u.username,
|
||||||
|
# u.firstname,
|
||||||
|
# u.lastname,
|
||||||
|
# u.role_id || ' | ' || r.role_text as role,
|
||||||
|
# u.new_pwd,
|
||||||
|
# u.active
|
||||||
|
# from
|
||||||
|
# users u
|
||||||
|
# left join roles r
|
||||||
|
# on u.role_id = r.role_id
|
||||||
|
# """)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# col_find_role, col_create_role, col_modify_role, col_delete_role = st.columns([3,2,2,2], vertical_alignment="bottom")
|
||||||
|
|
||||||
|
# with col_find_role:
|
||||||
|
# txt_search = st.text_input(label="Suche", label_visibility="hidden", placeholder="Benutzer, Vorname, ...", icon=":material/search:")
|
||||||
|
|
||||||
|
# with col_create_user:
|
||||||
|
# if st.button(label="Benutzer anlegen", use_container_width=True, icon=":material/person_add:"):
|
||||||
|
# dialog_create_user()
|
||||||
|
# if "save_msg" in st.session_state:
|
||||||
|
# st.toast(st.session_state.save_msg)
|
||||||
|
# del st.session_state.save_msg
|
||||||
|
|
||||||
|
# with col_modify_user:
|
||||||
|
# if st.button(label="Benutzer bearbeiten", use_container_width=True, icon=":material/person:"):
|
||||||
|
# if not st.session_state.selected_user_id is None:
|
||||||
|
# dialog_modify_user(st.session_state.selected_user_id)
|
||||||
|
# else:
|
||||||
|
# st.toast("❌ Bitte erst eine Zeile auswählen")
|
||||||
|
# if "save_msg" in st.session_state:
|
||||||
|
# st.toast(st.session_state.save_msg)
|
||||||
|
# del st.session_state.save_msg
|
||||||
|
|
||||||
|
# with col_delete_user:
|
||||||
|
# if st.button(label="Benutzer löschen", use_container_width=True, icon=":material/person_remove:"):
|
||||||
|
# if not st.session_state.selected_user_id is None:
|
||||||
|
# dialog_delete_user(st.session_state.selected_user_id)
|
||||||
|
# else:
|
||||||
|
# st.toast("❌ Bitte erst eine Zeile auswählen")
|
||||||
|
# if "delete_msg" in st.session_state:
|
||||||
|
# st.toast(st.session_state.delete_msg)
|
||||||
|
# del st.session_state.delete_msg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# if txt_search.strip():
|
||||||
|
# txt_search_norm = txt_search.strip().lower()
|
||||||
|
# mask = (
|
||||||
|
# df["username"].fillna("").str.lower().str.contains(txt_search_norm)
|
||||||
|
# | df["firstname"].fillna("").str.lower().str.contains(txt_search_norm)
|
||||||
|
# | df["lastname"].fillna("").str.lower().str.contains(txt_search_norm)
|
||||||
|
# )
|
||||||
|
# df_view = df.loc[mask].copy()
|
||||||
|
# else:
|
||||||
|
# df_view = df.copy()
|
||||||
|
|
||||||
|
# event = st.dataframe(df_view,key="data", on_select="rerun", selection_mode="single-row")
|
||||||
|
# rows = event.selection.rows
|
||||||
|
# if rows:
|
||||||
|
# row_idx = rows[0]
|
||||||
|
# st.session_state.selected_user_id = int(df_view.iloc[row_idx]["id"])
|
||||||
|
# else:
|
||||||
|
# st.session_state.selected_user_id = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sidebar()
|
||||||
31
app/pages/home.py
Normal file
31
app/pages/home.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import streamlit as st
|
||||||
|
from auth_runtime import require_login
|
||||||
|
from ui.sidebar import build_sidebar
|
||||||
|
from auth import get_fullname_for_user
|
||||||
|
import pandas as pd
|
||||||
|
from numpy.random import default_rng as rng
|
||||||
|
from tools.load_css import load_css
|
||||||
|
|
||||||
|
load_css()
|
||||||
|
|
||||||
|
st.set_page_config(page_title="Co-App Home", page_icon=":material/home:", layout="centered", initial_sidebar_state="expanded")
|
||||||
|
|
||||||
|
authenticator = require_login()
|
||||||
|
st.session_state["authenticator"] = authenticator
|
||||||
|
build_sidebar()
|
||||||
|
|
||||||
|
|
||||||
|
def home():
|
||||||
|
username = st.session_state.get("name")
|
||||||
|
st.header("Controlling-Portal")
|
||||||
|
st.info(f"Willkommen, {get_fullname_for_user(username)}!")
|
||||||
|
st.markdown("**Hier könnte eine Hinweistext für den Benutzer stehen**")
|
||||||
|
|
||||||
|
df = pd.DataFrame(rng(0).standard_normal((20, 3)), columns=["a", "b", "c"])
|
||||||
|
st.area_chart(df)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
home()
|
||||||
0
app/pages/roles.py
Normal file
0
app/pages/roles.py
Normal file
270
app/pages/user.py
Normal file
270
app/pages/user.py
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import streamlit as st
|
||||||
|
from auth_runtime import require_login
|
||||||
|
# from ui.sidebar import build_sidebar
|
||||||
|
from auth import create_user
|
||||||
|
from pathlib import Path
|
||||||
|
from tools.load_css import load_css
|
||||||
|
from app_db.app_db import get_list, send_cmd
|
||||||
|
from ui.selectboxes import get_roles, get_id
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
|
||||||
|
DASH_NAME = Path(__file__).stem # Hier muss die dash_id aus der DB stehen -> wird gegen die session_state geprüft (User-Berechtigung)
|
||||||
|
|
||||||
|
load_css()
|
||||||
|
|
||||||
|
st.set_page_config(page_title="Co-App Benutzer", page_icon=":material/person:", layout="wide", initial_sidebar_state="collapsed")
|
||||||
|
|
||||||
|
authenticator = require_login()
|
||||||
|
st.session_state["authenticator"] = authenticator
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def sidebar():
|
||||||
|
|
||||||
|
fullname = st.session_state.get("fullname")
|
||||||
|
role_text = st.session_state.get("role_text")
|
||||||
|
|
||||||
|
with st.sidebar:
|
||||||
|
|
||||||
|
st.logo("app/images/GMN_Logo_neu_rgb.png", size="small")
|
||||||
|
st.markdown(f"**{fullname}** ({role_text})")
|
||||||
|
|
||||||
|
col1, col2 = st.columns([2,2])
|
||||||
|
with col1:
|
||||||
|
authenticator.logout("Logout")
|
||||||
|
with col2:
|
||||||
|
if st.button("Home", use_container_width=True, icon=":material/home:"):
|
||||||
|
st.switch_page("pages/home.py")
|
||||||
|
|
||||||
|
user()
|
||||||
|
|
||||||
|
|
||||||
|
@st.dialog("Benutzer anlegen")
|
||||||
|
def dialog_create_user():
|
||||||
|
txt_username = st.text_input("Benutzername")
|
||||||
|
txt_firstname = st.text_input("Vorname")
|
||||||
|
txt_lastname = st.text_input("Nachname")
|
||||||
|
txt_email = st.text_input("Email")
|
||||||
|
txt_pwd = st.text_input("Kennwort", type="password")
|
||||||
|
cmb_role = st.selectbox("Rolle", get_roles(), index=None)
|
||||||
|
if st.button("Save"):
|
||||||
|
if create_user(
|
||||||
|
username=txt_username,
|
||||||
|
firstname=txt_firstname,
|
||||||
|
lastname=txt_lastname,
|
||||||
|
email=txt_email,
|
||||||
|
role_id=get_id(cmb_role),
|
||||||
|
password=txt_pwd
|
||||||
|
):
|
||||||
|
st.session_state.save_msg = f"✅ Benutzer '{txt_username}' erfolgreich gespeichert"
|
||||||
|
else:
|
||||||
|
st.session_state.save_msg = "❌ Fehler beim Speichern"
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
|
@st.dialog("Benutzer löschen")
|
||||||
|
def dialog_delete_user(id):
|
||||||
|
if id == None:
|
||||||
|
st.write("kein Benutzer ausgewählt")
|
||||||
|
else:
|
||||||
|
df = get_list("select username from users where id = ?",(id,))
|
||||||
|
username = df.iloc[0]["username"]
|
||||||
|
st.write(f"Der Benutzer {username} wird gelöscht! Sind Sie sicher?")
|
||||||
|
if st.button("Löschen"):
|
||||||
|
if username != "admin":
|
||||||
|
if send_cmd("delete from users where id = ?",(id,)):
|
||||||
|
st.session_state.delete_msg = f"✅ Benutzer '{username}' erfolgreich gelöscht!"
|
||||||
|
else:
|
||||||
|
st.session_state.delete_msg = f"❌ Benutzer '{username}' konnte nicht gelöscht werden!"
|
||||||
|
else:
|
||||||
|
st.session_state.delete_msg = f"❌ Benutzer '{username}' darf nicht gelöscht werden!"
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
|
@st.dialog("Benutzer bearbeiten")
|
||||||
|
def dialog_modify_user(id):
|
||||||
|
if id == None:
|
||||||
|
st.write("kein Benutzer ausgewählt")
|
||||||
|
else:
|
||||||
|
sql = """
|
||||||
|
select
|
||||||
|
u.id,
|
||||||
|
u.username as user, -- ACHTUNG: nicht mit username arbeiten, da Überschneidung in sessionstate!!
|
||||||
|
u.firstname,
|
||||||
|
u.lastname,
|
||||||
|
u.email,
|
||||||
|
u.role_id || ' | ' || r.role_text as role,
|
||||||
|
r.role_text,
|
||||||
|
u.new_pwd,
|
||||||
|
u.active
|
||||||
|
from
|
||||||
|
users u
|
||||||
|
left join roles r
|
||||||
|
on u.role_id = r.role_id
|
||||||
|
where u.id = ?
|
||||||
|
"""
|
||||||
|
df = get_list(sql,(id,))
|
||||||
|
|
||||||
|
|
||||||
|
# df = get_list("select username from users where id = ?",(id,))
|
||||||
|
|
||||||
|
# st.session_state.orig_user_data = df
|
||||||
|
|
||||||
|
|
||||||
|
df_roles = get_roles()
|
||||||
|
roles = df_roles["text"].tolist()
|
||||||
|
role = df.iloc[0]["role"]
|
||||||
|
try:
|
||||||
|
idx = roles.index(role)
|
||||||
|
except:
|
||||||
|
idx = None
|
||||||
|
|
||||||
|
txt_username = st.text_input(label="Benutzername", value=df.iloc[0]["user"])
|
||||||
|
txt_firstname = st.text_input(label="Vorname", value=df.iloc[0]["firstname"])
|
||||||
|
txt_lastname = st.text_input(label="Nachname", value=df.iloc[0]["lastname"])
|
||||||
|
txt_email = st.text_input(label="Email", value=df.iloc[0]["email"])
|
||||||
|
txt_pwd = st.text_input(label="Passwort", placeholder="Neues Passwort eingeben", type="password")
|
||||||
|
new_pwd = st.checkbox(label="Neues Passwort", value=df.iloc[0]["new_pwd"])
|
||||||
|
cmb_role = st.selectbox(label="Rolle", options=roles, placeholder="Rolle auswählen", index=idx)
|
||||||
|
|
||||||
|
|
||||||
|
if st.button("Save"):
|
||||||
|
pw_hash = bcrypt.hashpw(txt_pwd.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||||
|
|
||||||
|
if txt_pwd and txt_pwd.strip():
|
||||||
|
sql = """
|
||||||
|
update users set
|
||||||
|
username = ?,
|
||||||
|
firstname = ?,
|
||||||
|
lastname = ?,
|
||||||
|
email = ?,
|
||||||
|
password_hash = ?,
|
||||||
|
new_pwd = ?,
|
||||||
|
role_id = ?
|
||||||
|
where id = ?
|
||||||
|
"""
|
||||||
|
params = (txt_username, txt_firstname, txt_lastname, txt_email, pw_hash, new_pwd, get_id(cmb_role), id)
|
||||||
|
# send_cmd(sql,(txt_username, txt_firstname, txt_lastname, txt_email, pw_hash, new_pwd, get_id(cmb_role), id))
|
||||||
|
else:
|
||||||
|
sql = """
|
||||||
|
update users set
|
||||||
|
username = ?,
|
||||||
|
firstname = ?,
|
||||||
|
lastname = ?,
|
||||||
|
email = ?,
|
||||||
|
new_pwd = ?,
|
||||||
|
role_id = ?
|
||||||
|
where id = ?
|
||||||
|
"""
|
||||||
|
params = (txt_username, txt_firstname, txt_lastname, txt_email, new_pwd, get_id(cmb_role), id)
|
||||||
|
# send_cmd(sql,(txt_username, txt_firstname, txt_lastname, txt_email, new_pwd, get_id(cmb_role), id))
|
||||||
|
print (params)
|
||||||
|
if send_cmd(sql, params):
|
||||||
|
st.session_state.save_msg = f"✅ Benutzer '{txt_username}' erfolgreich geändert"
|
||||||
|
else:
|
||||||
|
st.session_state.save_msg = "❌ Fehler beim Speichern"
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def user():
|
||||||
|
|
||||||
|
if "selected_user_id" not in st.session_state:
|
||||||
|
st.session_state.selected_user_id = None
|
||||||
|
|
||||||
|
# tab_user, tab_role, tab_permission = st.tabs(["Benutzer", "Rollen", "Berechtigungen"])
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------------------------------------
|
||||||
|
# Benutzerverwaltung
|
||||||
|
#--------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# with tab_user:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
df = get_list("""
|
||||||
|
select
|
||||||
|
u.id,
|
||||||
|
u.username,
|
||||||
|
u.firstname,
|
||||||
|
u.lastname,
|
||||||
|
u.role_id || ' | ' || r.role_text as role,
|
||||||
|
u.new_pwd,
|
||||||
|
u.active
|
||||||
|
from
|
||||||
|
users u
|
||||||
|
left join roles r
|
||||||
|
on u.role_id = r.role_id
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
col_find_user, col_create_user, col_modify_user, col_delete_user = st.columns([3,2,2,2], vertical_alignment="bottom")
|
||||||
|
|
||||||
|
with col_find_user:
|
||||||
|
txt_search = st.text_input(label="Suche", label_visibility="hidden", placeholder="Benutzer, Vorname, ...", icon=":material/search:")
|
||||||
|
|
||||||
|
with col_create_user:
|
||||||
|
if st.button(label="Benutzer anlegen", use_container_width=True, icon=":material/person_add:"):
|
||||||
|
dialog_create_user()
|
||||||
|
if "save_msg" in st.session_state:
|
||||||
|
st.toast(st.session_state.save_msg)
|
||||||
|
del st.session_state.save_msg
|
||||||
|
|
||||||
|
with col_modify_user:
|
||||||
|
if st.button(label="Benutzer bearbeiten", use_container_width=True, icon=":material/person:"):
|
||||||
|
if not st.session_state.selected_user_id is None:
|
||||||
|
dialog_modify_user(st.session_state.selected_user_id)
|
||||||
|
else:
|
||||||
|
st.toast("❌ Bitte erst eine Zeile auswählen")
|
||||||
|
if "save_msg" in st.session_state:
|
||||||
|
st.toast(st.session_state.save_msg)
|
||||||
|
del st.session_state.save_msg
|
||||||
|
|
||||||
|
with col_delete_user:
|
||||||
|
if st.button(label="Benutzer löschen", use_container_width=True, icon=":material/person_remove:"):
|
||||||
|
if not st.session_state.selected_user_id is None:
|
||||||
|
dialog_delete_user(st.session_state.selected_user_id)
|
||||||
|
else:
|
||||||
|
st.toast("❌ Bitte erst eine Zeile auswählen")
|
||||||
|
if "delete_msg" in st.session_state:
|
||||||
|
st.toast(st.session_state.delete_msg)
|
||||||
|
del st.session_state.delete_msg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if txt_search.strip():
|
||||||
|
txt_search_norm = txt_search.strip().lower()
|
||||||
|
mask = (
|
||||||
|
df["username"].fillna("").str.lower().str.contains(txt_search_norm)
|
||||||
|
| df["firstname"].fillna("").str.lower().str.contains(txt_search_norm)
|
||||||
|
| df["lastname"].fillna("").str.lower().str.contains(txt_search_norm)
|
||||||
|
)
|
||||||
|
df_view = df.loc[mask].copy()
|
||||||
|
else:
|
||||||
|
df_view = df.copy()
|
||||||
|
|
||||||
|
event = st.dataframe(df_view,key="data", on_select="rerun", selection_mode="single-row")
|
||||||
|
rows = event.selection.rows
|
||||||
|
if rows:
|
||||||
|
row_idx = rows[0]
|
||||||
|
st.session_state.selected_user_id = int(df_view.iloc[row_idx]["id"])
|
||||||
|
else:
|
||||||
|
st.session_state.selected_user_id = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sidebar()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
8
app/tools/load_css.py
Normal file
8
app/tools/load_css.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import streamlit as st
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def load_css():
|
||||||
|
css_path = Path(__file__).parent.parent / ".streamlit" / "style.css"
|
||||||
|
if css_path.exists():
|
||||||
|
st.markdown(f"<style>{css_path.read_text()}</style>", unsafe_allow_html=True)
|
||||||
20
app/tools/numgen.py
Normal file
20
app/tools/numgen.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from app_db.app_db import get_value, send_cmd
|
||||||
|
from logging_config import setup_logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
APP_ENV = os.environ.get("APP_ENV", "dev")
|
||||||
|
logger = setup_logging(APP_ENV)
|
||||||
|
|
||||||
|
def get_num(numgen, step=10):
|
||||||
|
num = get_value(f"select param_value from param where parameter = '{numgen}'")
|
||||||
|
print(int(num) + step)
|
||||||
|
update_num(num, 10, numgen)
|
||||||
|
return num
|
||||||
|
|
||||||
|
def update_num(num, step, numgen):
|
||||||
|
try:
|
||||||
|
send_cmd(f"update param set param_value = {int(num) + step} where parameter = '{numgen}'")
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
logger.warning(f"Fehler beim Hochzählen von {numgen}. Bitte manuel ändern!")
|
||||||
|
return False
|
||||||
19
app/ui/selectboxes.py
Normal file
19
app/ui/selectboxes.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from app_db.app_db import get_list
|
||||||
|
|
||||||
|
def get_roles():
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
select
|
||||||
|
role_id || ' | ' || role_text as text
|
||||||
|
from
|
||||||
|
roles
|
||||||
|
"""
|
||||||
|
df = get_list(sql)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_id(id_text: str):
|
||||||
|
id = int(id_text.split("|")[0])
|
||||||
|
if not id:
|
||||||
|
id = ""
|
||||||
|
return id
|
||||||
98
app/ui/sidebar.py
Normal file
98
app/ui/sidebar.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from contextlib import closing
|
||||||
|
import streamlit as st
|
||||||
|
from auth import get_fullname_for_user, get_role_for_user
|
||||||
|
from app_db.app_db import get_conn, get_list
|
||||||
|
# import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
def build_sidebar():
|
||||||
|
|
||||||
|
authenticator = st.session_state.get("authenticator")
|
||||||
|
username = st.session_state.get("name")
|
||||||
|
role_text = st.session_state.get("role_text")
|
||||||
|
fullname = st.session_state.get("fullname")
|
||||||
|
|
||||||
|
if not authenticator or not username:
|
||||||
|
return
|
||||||
|
|
||||||
|
df = st.session_state.get("df_sidebar")
|
||||||
|
|
||||||
|
with st.sidebar:
|
||||||
|
|
||||||
|
st.logo("app/images/GMN_Logo_neu_rgb.png", size="small")
|
||||||
|
st.markdown(f"**{fullname}** ({role_text})")
|
||||||
|
col1, col2 = st.columns([2,2])
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
|
||||||
|
authenticator.logout("Logout")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
if st.button("Home", use_container_width=True, icon=":material/home:"):
|
||||||
|
st.switch_page("pages/home.py")
|
||||||
|
|
||||||
|
# --- Suchfeld ---
|
||||||
|
query = st.text_input("Menü-Suche", "", placeholder="z.B. Umsatz, Kosten, User ...")
|
||||||
|
query = query.strip()
|
||||||
|
|
||||||
|
# Aktive Seite ermitteln (für Expander-Status / Highlight)
|
||||||
|
current_page = st.session_state.get("_page_path")
|
||||||
|
|
||||||
|
# --- DF filtern, falls Suchbegriff gesetzt ---
|
||||||
|
if query:
|
||||||
|
mask = (
|
||||||
|
df["dash_text"].str.contains(query, case=False, na=False)
|
||||||
|
| df["group_text"].str.contains(query, case=False, na=False)
|
||||||
|
)
|
||||||
|
df_view = df[mask].copy()
|
||||||
|
else:
|
||||||
|
df_view = df
|
||||||
|
|
||||||
|
if df_view.empty:
|
||||||
|
st.info("Keine Einträge zum Suchbegriff gefunden.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Gruppiert durchlaufen ---
|
||||||
|
# df_view = df_view.sort_values(["group_order", "dash_order"]) # vorher noch sortieren
|
||||||
|
for group_text, df_group in df_view.groupby("group_text", sort=False): # und beim gruppieren nicht nach der Gruppe sortieren!
|
||||||
|
# Expander offen, wenn:
|
||||||
|
# - aktuelle Seite in dieser Gruppe liegt
|
||||||
|
group_open = any(
|
||||||
|
(row["page_file"] == current_page)
|
||||||
|
for _, row in df_group.iterrows()
|
||||||
|
)
|
||||||
|
|
||||||
|
with st.expander(group_text, expanded=(group_open or bool(query))):
|
||||||
|
for _, row in df_group.iterrows():
|
||||||
|
dash_type = row.get("dash_type")
|
||||||
|
page_file = row.get("page_file")
|
||||||
|
label = row.get("dash_text", "")
|
||||||
|
|
||||||
|
# Streamlit-Page
|
||||||
|
if dash_type == "page" and isinstance(page_file, str) and page_file.strip():
|
||||||
|
st.page_link(f"pages/{page_file}.py", label=label, help="Das ist die Hilfe")
|
||||||
|
|
||||||
|
# Externer Link
|
||||||
|
elif dash_type == "url" and isinstance(page_file, str) and page_file.strip():
|
||||||
|
st.markdown(f"[{label}]({page_file})")
|
||||||
|
|
||||||
|
# Platzhalter / Sonstiges
|
||||||
|
else:
|
||||||
|
st.write(f"▫️ {label}")
|
||||||
|
|
||||||
|
# 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)
|
||||||
1
app/version.py
Normal file
1
app/version.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.0.1"
|
||||||
7
config/auth.yaml
Normal file
7
config/auth.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
cookie:
|
||||||
|
expiry_days: 7
|
||||||
|
key: "tHr1FXOYg-xsYBGVu7amRrY8PAdC2gM_zyjPEPPkPgG8tAsLY4QGPZvCvMS9win0D94uawAxsmZjN6O_VlRhUQ"
|
||||||
|
name: "co_app-session"
|
||||||
|
|
||||||
|
preauthorized:
|
||||||
|
emails: []
|
||||||
10
config/settings.env
Normal file
10
config/settings.env
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Databases
|
||||||
|
|
||||||
|
# Oracle
|
||||||
|
oracle_conn_str=oracle+oracledb://sp84p:data@ora:1522/?service_name=prod84
|
||||||
|
|
||||||
|
# gmn-conn\co_dw
|
||||||
|
co_dw_conn_str = mssql+pyodbc://co_app:JRHmi1KLwjgnF6@gmn-cont\controlling/co_dw?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes
|
||||||
|
|
||||||
|
# gmn-conn\co_daten
|
||||||
|
co_daten_conn_str = mssql+pyodbc://dw_user:Schneewittchen%4089887%%21@gmn-cont\controlling/co_daten?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes
|
||||||
20
migrations/20251128_210200_initial.sql
Normal file
20
migrations/20251128_210200_initial.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_version (
|
||||||
|
version TEXT PRIMARY KEY,
|
||||||
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
date_create TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash BLOB NOT NULL,
|
||||||
|
role TEXT NOT NULL DEFAULT 'guest'
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251128_210200_initial');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
13
migrations/20251129_162900_add_table_sessions.sql
Normal file
13
migrations/20251129_162900_add_table_sessions.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
expires_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251129_162900_add_table_sessions');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
8
migrations/20251130_124700_add_col_email_users.sql
Normal file
8
migrations/20251130_124700_add_col_email_users.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column email text not null;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251130_124700_add_col_email_users');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
11
migrations/20251130_1411_add_col_name_users.sql
Normal file
11
migrations/20251130_1411_add_col_name_users.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column firstname text;
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column lastname text;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251130_1411_add_col_name_users');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
8
migrations/20251130_161100_add_col_newpwd_users.sql
Normal file
8
migrations/20251130_161100_add_col_newpwd_users.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table users
|
||||||
|
add column new_pwd integer not null default 1;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251130_161100_add_col_newpwd_users');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
42
migrations/20251130_191100_add_table_dashboards.sql
Normal file
42
migrations/20251130_191100_add_table_dashboards.sql
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
create table if not exists dashboards (
|
||||||
|
dash_id integer unique not null,
|
||||||
|
dash_text text not null,
|
||||||
|
dash_description text,
|
||||||
|
group_id integer not null,
|
||||||
|
active integer not null default 1,
|
||||||
|
date_create TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
id integer primary key autoincrement
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists groups (
|
||||||
|
group_id integer unique not null,
|
||||||
|
group_text text not null,
|
||||||
|
group_description text,
|
||||||
|
active integer not null default 1,
|
||||||
|
date_create TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
id integer primary key autoincrement
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists roles (
|
||||||
|
role_id integer unique not null,
|
||||||
|
role_text text not null,
|
||||||
|
role_description text,
|
||||||
|
active integer not null default 1,
|
||||||
|
date_create TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
id integer primary key autoincrement
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists permissions (
|
||||||
|
role_id integer not null,
|
||||||
|
dash_id integer not null,
|
||||||
|
active integer not null default 1,
|
||||||
|
date_create TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
unique (role_id, dash_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251130_191100_add_table_dashboards');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
14
migrations/20251130_200000_add_col_area.sql
Normal file
14
migrations/20251130_200000_add_col_area.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table dashboards
|
||||||
|
add column zgrp1 text;
|
||||||
|
|
||||||
|
alter table dashboards
|
||||||
|
add column zgrp2 text;
|
||||||
|
|
||||||
|
alter table dashboards
|
||||||
|
add column zgrp3 text;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251130_200000_add_col_area');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
11
migrations/20251130_200600_add_col_order.sql
Normal file
11
migrations/20251130_200600_add_col_order.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table dashboards
|
||||||
|
add column order_no integer default 0;
|
||||||
|
|
||||||
|
alter table groups
|
||||||
|
add column order_no integer default 0;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251130_200600_add_col_order');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
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;
|
||||||
11
migrations/20251204_210700_add_col_page_dashboards.sql
Normal file
11
migrations/20251204_210700_add_col_page_dashboards.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table dashboards
|
||||||
|
add column page_file text;
|
||||||
|
|
||||||
|
alter table dashboards
|
||||||
|
add column dash_type text;
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251204_210700_add_col_page_dashboards');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
14
migrations/20251218_211700_add_table_param.sql
Normal file
14
migrations/20251218_211700_add_table_param.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
create table if not exists param (
|
||||||
|
parameter text unique not null,
|
||||||
|
parameter_text text not null,
|
||||||
|
param_description text,
|
||||||
|
param_value text not null,
|
||||||
|
date_create TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
id integer primary key autoincrement
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20251218_211700_add_table_param');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user