Ověření přihlašovacích údajů před uložením do správce autentizace

Špatné heslo zadané v přihlašovacím dialogu se dosud uložilo do QGIS
Authentication Manageru a chyba se ukázala až po zavření dialogu.

- Dialog nyní před uložením zkusí přihlášení k API; při neplatných
  údajích se nic neuloží a uživatel může údaje rovnou opravit.
- Pokud je server nedostupný (údaje nelze ověřit), uživatel si může
  zvolit, zda je uložit neověřené – změna hesla na webu u dříve
  uložených údajů tím není dotčena.
- login_to_api nově rozlišuje důvod selhání (LAST_LOGIN_ERROR:
  'auth' × 'network'), aby dialog uměl oba případy odlišit.
- Ověřuje se i změna samotného e-mailu (proti uloženému heslu).
This commit is contained in:
Claude
2026-06-12 10:49:59 +00:00
committed by David Spáčil
parent 93ed0ca810
commit fd11bee274
2 changed files with 58 additions and 2 deletions
+43
View File
@@ -627,6 +627,39 @@ class LoginDialog(QDialog):
return True return True
def _verify_credentials(self, username: str, password: str) -> bool:
"""
Verify the credentials against the API before saving them.
Returns True if they should be stored: either the login succeeded,
or the server was unreachable and the user chose to keep them
unverified. Wrong credentials are never stored.
"""
# Lazy import to avoid an import cycle
# (amcr_tools imports LoginDialog lazily as well)
from . import amcr_tools
if amcr_tools.login_to_api(username, password):
return True
if amcr_tools.LAST_LOGIN_ERROR == 'network':
answer = QMessageBox.question(
self,
"Server nedostupný",
"Přihlašovací údaje se nepodařilo ověřit server AMČR "
"je nedostupný.\nChcete je přesto uložit (neověřené)?",
QMessageBox.StandardButton.Yes
| QMessageBox.StandardButton.No
)
return answer == QMessageBox.StandardButton.Yes
QMessageBox.warning(
self,
"Neplatné přihlašovací údaje",
"Přihlášení se nezdařilo zkontrolujte e-mail a heslo.\n"
"Údaje nebyly uloženy."
)
return False
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Button actions # Button actions
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -653,6 +686,11 @@ class LoginDialog(QDialog):
if ok: if ok:
if not self._ensure_master_password(): if not self._ensure_master_password():
return return
# Verify the new username against the stored password
if not self._verify_credentials(
username, cfg.config("password", "")
):
return
cfg.setConfig("username", username) cfg.setConfig("username", username)
auth_mgr.updateAuthenticationConfig(cfg) auth_mgr.updateAuthenticationConfig(cfg)
self.accept() self.accept()
@@ -662,6 +700,11 @@ class LoginDialog(QDialog):
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.") QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.")
return return
# Verify before prompting for the master password wrong
# credentials must never reach the Authentication Manager
if not self._verify_credentials(username, password):
return
if not self._ensure_master_password(): if not self._ensure_master_password():
return return
+15 -2
View File
@@ -17,6 +17,10 @@ TRANSLATIONS = {}
# None = not logged in (anonymous access) # None = not logged in (anonymous access)
AMCR_SESSION: requests.Session | None = None AMCR_SESSION: requests.Session | None = None
# Reason of the last failed login: 'auth' (wrong credentials),
# 'network' (server unreachable / invalid response) or None
LAST_LOGIN_ERROR: str | None = None
# Re-entrancy guard: the download runs in the main thread and pumps the # Re-entrancy guard: the download runs in the main thread and pumps the
# event loop via processEvents(), so the user could otherwise start # event loop via processEvents(), so the user could otherwise start
# a second download while the first one is still running # a second download while the first one is still running
@@ -38,6 +42,9 @@ def login_to_api(username: str, password: str):
""" """
login_url = "https://digiarchiv.aiscr.cz/api/user/login" login_url = "https://digiarchiv.aiscr.cz/api/user/login"
global LAST_LOGIN_ERROR
LAST_LOGIN_ERROR = None
_log(f"Přihlašuji uživatele: '{username}'") _log(f"Přihlašuji uživatele: '{username}'")
if not username or not password: if not username or not password:
@@ -45,6 +52,7 @@ def login_to_api(username: str, password: str):
"CHYBA: username nebo heslo je prázdné.", "CHYBA: username nebo heslo je prázdné.",
Qgis.MessageLevel.Critical Qgis.MessageLevel.Critical
) )
LAST_LOGIN_ERROR = 'auth'
return None return None
session = requests.Session() session = requests.Session()
@@ -72,6 +80,7 @@ def login_to_api(username: str, password: str):
f"CHYBA přihlášení (API): {body['error']}", f"CHYBA přihlášení (API): {body['error']}",
Qgis.MessageLevel.Critical Qgis.MessageLevel.Critical
) )
LAST_LOGIN_ERROR = 'auth'
return None return None
_log("Přihlášení proběhlo úspěšně.") _log("Přihlášení proběhlo úspěšně.")
@@ -80,18 +89,22 @@ def login_to_api(username: str, password: str):
return session return session
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
_log(f"CHYBA HTTP {e.response.status_code if e.response else '?'}: " status = e.response.status_code if e.response is not None else None
f"{e.response.text[:300] if e.response else 'žádná odpověď'}", _log(f"CHYBA HTTP {status if status else '?'}: "
f"{e.response.text[:300] if e.response is not None else 'žádná odpověď'}",
Qgis.MessageLevel.Critical) Qgis.MessageLevel.Critical)
LAST_LOGIN_ERROR = 'auth' if status in (401, 403) else 'network'
return None return None
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical) _log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
LAST_LOGIN_ERROR = 'network'
return None return None
except ValueError: except ValueError:
# Server returned non-JSON (e.g. an HTML error page behind a proxy) # Server returned non-JSON (e.g. an HTML error page behind a proxy)
_log("CHYBA: server nevrátil platný JSON: " _log("CHYBA: server nevrátil platný JSON: "
f"{response.text[:300]}", f"{response.text[:300]}",
Qgis.MessageLevel.Critical) Qgis.MessageLevel.Critical)
LAST_LOGIN_ERROR = 'network'
return None return None