Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +23 -24
src/streamlit_app.py
CHANGED
|
@@ -17,10 +17,10 @@ st.title("🔐 Simple Password Lookup — 2FA protected")
|
|
| 17 |
|
| 18 |
# ---------------- Persistence ----------------
|
| 19 |
PERSIST_DIR = Path("/data") if Path("/data").exists() else Path(".")
|
| 20 |
-
PERSIST_FILE = PERSIST_DIR / "creds.xlsx" # saved credentials
|
| 21 |
-
CONFIG_FILE = PERSIST_DIR / "auth.json" #
|
| 22 |
|
| 23 |
-
# ----------------
|
| 24 |
ph = PasswordHasher()
|
| 25 |
ALIASES = {
|
| 26 |
"name": ["name", "title", "site", "account", "platform", "service", "app"],
|
|
@@ -53,6 +53,7 @@ def standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 53 |
return out[mask].reset_index(drop=True)
|
| 54 |
|
| 55 |
def read_any(file) -> pd.DataFrame:
|
|
|
|
| 56 |
name = (getattr(file, "name", "") or "").lower()
|
| 57 |
if name.endswith(".csv"):
|
| 58 |
return pd.read_csv(file)
|
|
@@ -78,13 +79,10 @@ if "authed" not in st.session_state:
|
|
| 78 |
st.session_state.authed = False
|
| 79 |
if "failures" not in st.session_state:
|
| 80 |
st.session_state.failures = 0
|
| 81 |
-
# setup state (used
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
st.session_state.setup_label = None
|
| 86 |
-
if "setup_pw" not in st.session_state:
|
| 87 |
-
st.session_state.setup_pw = None
|
| 88 |
|
| 89 |
# ---------------- Step 1: Upload (only if not saved yet) ----------------
|
| 90 |
if not PERSIST_FILE.exists():
|
|
@@ -110,24 +108,26 @@ if not cfg:
|
|
| 110 |
|
| 111 |
# Collect label + password; generate secret ONCE and keep it in session
|
| 112 |
with st.form("setup_form"):
|
| 113 |
-
label = st.text_input(
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
| 116 |
gen = st.form_submit_button("Generate QR code")
|
| 117 |
if gen:
|
| 118 |
if not pw1 or pw1 != pw2:
|
| 119 |
st.error("Passwords are empty or do not match.")
|
| 120 |
else:
|
| 121 |
-
|
| 122 |
-
if not st.session_state.setup_secret:
|
| 123 |
st.session_state.setup_secret = pyotp.random_base32()
|
| 124 |
st.session_state.setup_label = label
|
| 125 |
st.session_state.setup_pw = pw1
|
| 126 |
|
| 127 |
# If secret staged, show QR and verify input
|
| 128 |
-
if st.session_state.setup_secret:
|
| 129 |
secret = st.session_state.setup_secret
|
| 130 |
-
label = st.session_state.setup_label or "SimpleLookup"
|
| 131 |
totp = pyotp.TOTP(secret)
|
| 132 |
uri = totp.provisioning_uri(name=label, issuer_name="Simple Password Lookup")
|
| 133 |
st.image(make_qr_png(uri), caption="Scan in Google/Microsoft Authenticator")
|
|
@@ -135,16 +135,15 @@ if not cfg:
|
|
| 135 |
code = st.text_input("Enter a current 6-digit code to verify", max_chars=6)
|
| 136 |
if st.button("Verify & Save"):
|
| 137 |
if totp.verify(code, valid_window=1):
|
| 138 |
-
# hash password and save config
|
| 139 |
hash_pw = ph.hash(st.session_state.setup_pw)
|
| 140 |
save_config({"password_hash": hash_pw, "totp_secret": secret, "label": label})
|
| 141 |
-
# clear setup state
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
st.
|
| 145 |
-
st.
|
| 146 |
else:
|
| 147 |
-
st.error("Invalid code.
|
| 148 |
st.stop()
|
| 149 |
|
| 150 |
# ---------------- Step 3: Login (password + 2FA) ----------------
|
|
|
|
| 17 |
|
| 18 |
# ---------------- Persistence ----------------
|
| 19 |
PERSIST_DIR = Path("/data") if Path("/data").exists() else Path(".")
|
| 20 |
+
PERSIST_FILE = PERSIST_DIR / "creds.xlsx" # saved credentials (uploaded once)
|
| 21 |
+
CONFIG_FILE = PERSIST_DIR / "auth.json" # Argon2 password hash + TOTP secret + label
|
| 22 |
|
| 23 |
+
# ---------------- Helpers ----------------
|
| 24 |
ph = PasswordHasher()
|
| 25 |
ALIASES = {
|
| 26 |
"name": ["name", "title", "site", "account", "platform", "service", "app"],
|
|
|
|
| 53 |
return out[mask].reset_index(drop=True)
|
| 54 |
|
| 55 |
def read_any(file) -> pd.DataFrame:
|
| 56 |
+
"""Read CSV or Excel from an uploaded file-like object."""
|
| 57 |
name = (getattr(file, "name", "") or "").lower()
|
| 58 |
if name.endswith(".csv"):
|
| 59 |
return pd.read_csv(file)
|
|
|
|
| 79 |
st.session_state.authed = False
|
| 80 |
if "failures" not in st.session_state:
|
| 81 |
st.session_state.failures = 0
|
| 82 |
+
# setup state (only used during first-time 2FA setup)
|
| 83 |
+
st.session_state.setdefault("setup_secret", None)
|
| 84 |
+
st.session_state.setdefault("setup_label", None)
|
| 85 |
+
st.session_state.setdefault("setup_pw", None)
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
# ---------------- Step 1: Upload (only if not saved yet) ----------------
|
| 88 |
if not PERSIST_FILE.exists():
|
|
|
|
| 108 |
|
| 109 |
# Collect label + password; generate secret ONCE and keep it in session
|
| 110 |
with st.form("setup_form"):
|
| 111 |
+
label = st.text_input(
|
| 112 |
+
"Account label (as shown in your authenticator)",
|
| 113 |
+
value=st.session_state.get("setup_label") or "SimpleLookup"
|
| 114 |
+
)
|
| 115 |
+
pw1 = st.text_input("New admin password", type="password", value=st.session_state.get("setup_pw") or "")
|
| 116 |
+
pw2 = st.text_input("Confirm password", type="password", value=st.session_state.get("setup_pw") or "")
|
| 117 |
gen = st.form_submit_button("Generate QR code")
|
| 118 |
if gen:
|
| 119 |
if not pw1 or pw1 != pw2:
|
| 120 |
st.error("Passwords are empty or do not match.")
|
| 121 |
else:
|
| 122 |
+
if not st.session_state.get("setup_secret"):
|
|
|
|
| 123 |
st.session_state.setup_secret = pyotp.random_base32()
|
| 124 |
st.session_state.setup_label = label
|
| 125 |
st.session_state.setup_pw = pw1
|
| 126 |
|
| 127 |
# If secret staged, show QR and verify input
|
| 128 |
+
if st.session_state.get("setup_secret"):
|
| 129 |
secret = st.session_state.setup_secret
|
| 130 |
+
label = st.session_state.get("setup_label") or "SimpleLookup"
|
| 131 |
totp = pyotp.TOTP(secret)
|
| 132 |
uri = totp.provisioning_uri(name=label, issuer_name="Simple Password Lookup")
|
| 133 |
st.image(make_qr_png(uri), caption="Scan in Google/Microsoft Authenticator")
|
|
|
|
| 135 |
code = st.text_input("Enter a current 6-digit code to verify", max_chars=6)
|
| 136 |
if st.button("Verify & Save"):
|
| 137 |
if totp.verify(code, valid_window=1):
|
|
|
|
| 138 |
hash_pw = ph.hash(st.session_state.setup_pw)
|
| 139 |
save_config({"password_hash": hash_pw, "totp_secret": secret, "label": label})
|
| 140 |
+
# clear transient setup state
|
| 141 |
+
for k in ("setup_secret", "setup_label", "setup_pw"):
|
| 142 |
+
st.session_state.pop(k, None)
|
| 143 |
+
st.success("2FA setup complete. Loading login…")
|
| 144 |
+
st.experimental_rerun() # 🔁 rerun so cfg loads and login form appears
|
| 145 |
else:
|
| 146 |
+
st.error("Invalid code. Try a fresh code from your authenticator.")
|
| 147 |
st.stop()
|
| 148 |
|
| 149 |
# ---------------- Step 3: Login (password + 2FA) ----------------
|