fix: před stahováním ověřit přihlášení přes islogged (#72) (#80)

* fix: před stahováním ověřit přihlášení přes islogged

Server při vypršelé session nevrací chybu, ale tiše odpoví jako
anonymnímu uživateli (jen přístupnost A). Plugin proto před stahováním
volá /api/user/islogged; při 'nologged' se jednou znovu přihlásí
z uložených údajů, a když to nejde, varuje v liště, že stahuje anonymně.

- amcr_tools: _check_islogged, _ensure_logged_in, volání v load_amcr_data
- smoke test: 7 offline scénářů stavu přihlášení
- README; changelog v2.2.0 v metadata.txt
- openspec/changes/fix-session-expiry-islogged: návrh, spec, design, úkoly

Closes #72. Připraveno s pomocí AI (Claude), ručně zkontrolováno.

* feat: odebrání přihlašovacích údajů uživatele i odhlásí

Dosud zůstala přihlášená session v paměti až do restartu QGIS, takže
se po „Odebrat uložené přihlašovací údaje“ dál stahovalo jako přihlášený.
Nově se session odhlásí na serveru (GET /api/user/logout) a zahodí;
když server neodpoví, zahodí se aspoň lokálně.

- amcr_tools: logout_from_api; amcr_dialog: volání v _forget_credentials
- smoke test: odhlášení, chyba sítě, bez session
- README, changelog v2.2.0, OpenSpec (požadavek + úkoly 2b; 3.3 ověřeno)

Připraveno s pomocí AI (Claude), ručně zkontrolováno.

* chore: archivovat OpenSpec změnu fix-session-expiry-islogged

Všechny úkoly hotové (2b.2 – odhlášení – ověřeno ručně v QGIS),
archivováno s --skip-specs (stupeň change-tracked, bez openspec/specs/).

Připraveno s pomocí AI (Claude), ručně zkontrolováno.
This commit is contained in:
david-spacil authored and GitHub committed 2026-10-02 12:37:31 +02:00
1 parent e5b0716fd6
commit 74090a80c6
10 files changed
+614 -10

No files matched your search

+15 -5
View File
@@ -1033,6 +1033,10 @@ class LoginDialog(QDialog):
self.accept()
def _forget_credentials(self):
# Lazy import to avoid an import cycle
# (amcr_tools imports LoginDialog lazily as well)
from . import amcr_tools
settings = QSettings()
existing_id = settings.value(self.SETTINGS_KEY, "")
if existing_id:
@@ -1040,11 +1044,17 @@ class LoginDialog(QDialog):
existing_id
)
settings.remove(self.SETTINGS_KEY)
QMessageBox.information(
self,
"Hotovo",
"Uložené přihlašovací údaje byly odebrány."
)
# Without credentials the session in memory would otherwise stay
# logged in until QGIS is restarted
if amcr_tools.logout_from_api():
zprava = ("Uložené přihlašovací údaje byly odebrány "
"a uživatel byl odhlášen.")
else:
zprava = ("Uložené přihlašovací údaje byly odebrány. Server "
"se nepodařilo kontaktovat, další stahování ale "
"proběhne anonymně.")
QMessageBox.information(self, "Hotovo", zprava)
self.reject()
# ------------------------------------------------------------------
+147 -1
View File
@@ -157,6 +157,125 @@ def _get_session() -> requests.Session | None:
return AMCR_SESSION
def logout_from_api() -> bool:
"""
Logs the current session out on the server (GET /api/user/logout)
and drops it from memory, so the next download runs anonymously
(or logs in again only if credentials are stored).
The local session is dropped even when the server cannot be
reached. Returns True when the server confirmed the logout or there
was no session at all.
"""
global AMCR_SESSION
session = AMCR_SESSION
AMCR_SESSION = None
if session is None:
return True
url = "https://digiarchiv.aiscr.cz/api/user/logout"
try:
response = session.get(url, timeout=10)
response.raise_for_status()
except requests.exceptions.RequestException as e:
_log(f"Odhlášení na serveru se nezdařilo: {e} – session "
"zahozena jen lokálně.", Qgis.MessageLevel.Warning)
return False
_log("Uživatel odhlášen.")
return True
def _check_islogged(session) -> bool | None:
"""
Asks the server whether the session is logged in
(GET /api/user/islogged; the check does not extend the session).
Returns True when logged in, False when the server reports
'nologged', or None when the check itself failed (network error,
invalid JSON) or the response has an unknown shape.
"""
url = "https://digiarchiv.aiscr.cz/api/user/islogged"
try:
body = session.get(url, timeout=10).json()
except (requests.exceptions.RequestException, ValueError) as e:
_log(f"Stav přihlášení se nepodařilo ověřit: {e}",
Qgis.MessageLevel.Warning)
return None
if not isinstance(body, dict):
_log("Neznámý formát odpovědi islogged – pokračuji dál.",
Qgis.MessageLevel.Warning)
return None
if "remaining" in body:
# Only the remaining seconds are logged, never a user profile
_log(f"Session je přihlášená (zbývá {body['remaining']} s).")
return True
if body.get("error"):
_log(f"Server session neuznává ({body['error']}).",
Qgis.MessageLevel.Warning)
return False
_log("Neznámý formát odpovědi islogged – pokračuji dál.",
Qgis.MessageLevel.Warning)
return None
def _ensure_logged_in() -> str:
"""
Verifies before a download that the user is logged in, whenever
a session exists or credentials are stored; renews the session
once when it has expired. Returns one of:
* "anonymous" – no session and no stored credentials (nothing
to check, no request is sent)
* "logged_in" – the current session is valid
* "relogged" – the session had expired and was renewed
* "fallback" – a login was expected but could not be established;
the download will run anonymously
* "unknown" – the check itself failed; the download proceeds
with the current session
"""
global AMCR_SESSION
session = _get_session()
if session is None:
# _get_session() has already tried the stored credentials;
# their presence therefore means the login failed
from .amcr_dialog import LoginDialog
username, password = LoginDialog.get_credentials()
if username and password:
_log("Přihlášení se nezdařilo – stahuji anonymně.",
Qgis.MessageLevel.Warning)
return "fallback"
return "anonymous"
stav = _check_islogged(session)
if stav is None:
return "unknown"
if stav:
return "logged_in"
# The server no longer accepts the session – drop it and try
# one re-login with the stored credentials
AMCR_SESSION = None
from .amcr_dialog import LoginDialog
username, password = LoginDialog.get_credentials()
if not (username and password):
_log("Session vypršela a přihlašovací údaje nejsou uloženy "
"– stahuji anonymně.", Qgis.MessageLevel.Warning)
return "fallback"
session = login_to_api(username, password)
if session is None:
return "fallback"
stav = _check_islogged(session)
if stav is None:
return "unknown"
if stav:
return "relogged"
_log("Nová session nebyla serverem uznána – stahuji anonymně.",
Qgis.MessageLevel.Warning)
return "fallback"
def _api_get_json(url, params, timeout=30) -> dict:
"""
Performs a GET request and returns the parsed JSON body.
@@ -168,7 +287,13 @@ def _api_get_json(url, params, timeout=30) -> dict:
def _is_auth_error(resp: requests.Response, body) -> bool:
"""The API returns auth errors with status 200 –
the body must be checked."""
the body must be checked.
Fallback only: the current server signals an expired session
by silently answering as anonymous (HTTP 200, no 'error'), so
this check never triggers on expiry today – the login state is
verified upfront by _ensure_logged_in() instead. Kept for the
day the API starts returning 401 or an explicit error text."""
if resp.status_code == 401:
return True
if not isinstance(body, dict):
@@ -292,6 +417,27 @@ def load_amcr_data(canvas, bb, filters=None,
return
_LOADING = True
# Login state is verified before the first query: an expired
# session would otherwise silently degrade the result to
# access level A without any error
try:
login_stav = _ensure_logged_in()
except Exception as e:
# The check must never block the download nor leave _LOADING
# stuck – an unexpected error means "proceed as today"
QgsMessageLog.logMessage(
f"Ověření stavu přihlášení selhalo: {e}",
"AMČR", Qgis.MessageLevel.Warning
)
login_stav = "unknown"
if login_stav == "fallback":
iface.messageBar().pushMessage(
"AMCR",
"Přihlášení se nepodařilo obnovit – stahování proběhne "
"anonymně a bude obsahovat jen záznamy s přístupností A.",
level=Qgis.MessageLevel.Warning
)
load_translations()
# --- 1. COORDINATE TRANSFORMATION ---
+3
View File
@@ -28,6 +28,9 @@ changelog=
* Filter labels unified: "Specifikace nálezu" renamed to "Materiál" to match the attribute alias
* README rewritten to match the current state of the code
* Tables for Akce and Lokality contain a field with feature weight, when Komponenty rendering is enabled
* The login state is verified before each download via /api/user/islogged; an expired session is renewed automatically
* When a logged-in download falls back to anonymous access, a message bar warning says so (only access level A data)
* Removing the stored credentials also logs the user out of the Digital Archive
v2.1.4 (2026-10-01)
* Removed unused generated resources.py and the bundled flake8 config, so the plugin passes the plugins.qgis.org scan without custom configuration
v2.1.3 (2026-10-01)