diff --git a/src/app.py b/src/app.py index 913d23b..fdb5023 100644 --- a/src/app.py +++ b/src/app.py @@ -67,6 +67,7 @@ app: Dash = Dash( server=server, title="decp.info", use_pages=True, + suppress_callback_exceptions=True, compress=True, meta_tags=META_TAGS, ) diff --git a/src/assets/css/style.css b/src/assets/css/style.css index ed1681f..6fe3c2f 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -78,10 +78,6 @@ button[disabled] { color: #666; } -button:hover:not([disabled]) { - background-color: #fee; -} - /* Global Link Styles */ #_pages_content a { color: #993333; diff --git a/src/auth/oauth.py b/src/auth/oauth.py index 1e73b28..a53f4bd 100644 --- a/src/auth/oauth.py +++ b/src/auth/oauth.py @@ -17,5 +17,8 @@ def init_oauth(app: Flask) -> None: client_id=os.getenv("LINKEDIN_CLIENT_ID"), client_secret=os.getenv("LINKEDIN_CLIENT_SECRET"), server_metadata_url=LINKEDIN_DISCOVERY_URL, - client_kwargs={"scope": "openid profile email"}, + client_kwargs={ + "scope": "openid profile email", + "token_endpoint_auth_method": "client_secret_post", + }, ) diff --git a/src/auth/routes.py b/src/auth/routes.py index 57b2281..ea1d217 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -264,12 +264,22 @@ def linkedin_callback(): # L'utilisateur a refusé / annulé l'autorisation côté LinkedIn. return _redirect_with_error("/connexion", "oauth_cancelled") try: - token = oauth.linkedin.authorize_access_token() + # LinkedIn ne retourne pas le nonce dans l'ID token (non-conformité OIDC) : + # authorize_access_token() lève MissingClaimError("nonce") après avoir échangé + # le code avec succès. Le token est déjà stocké dans oauth.linkedin.token à ce + # moment, donc on capture cette erreur précise et on continue. + try: + oauth.linkedin.authorize_access_token() + except Exception as exc: + if "nonce" not in str(exc) or not oauth.linkedin.token: + raise + resp = oauth.linkedin.get("https://api.linkedin.com/v2/userinfo") + resp.raise_for_status() + userinfo = resp.json() except Exception: logger.exception("Échec de l'échange de token LinkedIn") return _redirect_with_error("/connexion", "oauth_failed") - userinfo = token.get("userinfo") or {} subject = userinfo.get("sub") email = (userinfo.get("email") or "").strip().lower() if not subject or not email: diff --git a/src/db.py b/src/db.py index cc148ed..0152d6d 100644 --- a/src/db.py +++ b/src/db.py @@ -14,13 +14,16 @@ from src.utils import get_last_modified, logger def should_rebuild(db_path: Path, parquet_path: str) -> bool: db_path = Path(db_path) if not db_path.exists(): + logger.info("Fichier DuckDB inexistant.") return True dev = os.getenv("DEVELOPMENT", "False").lower() == "true" force = os.getenv("REBUILD_DUCKDB", "False").lower() == "true" if dev and not force: return False last_modified: float = get_last_modified(parquet_path) - return last_modified > db_path.stat().st_mtime + fresh_parquet = last_modified > db_path.stat().st_mtime + logger.info(f"Parquet plus récent : {str(fresh_parquet)}") + return fresh_parquet def _load_source_frame() -> pl.DataFrame: diff --git a/src/pages/compte_admin.py b/src/pages/compte_admin.py index 09143a9..812d92b 100644 --- a/src/pages/compte_admin.py +++ b/src/pages/compte_admin.py @@ -200,7 +200,6 @@ def layout( _password_section(), html.Hr(className="mt-4"), _danger_section(), - _logout_section(), ] ) return account_shell("admin", contenu) diff --git a/tests/test.parquet b/tests/test.parquet index d75858a..9b8fc9b 100644 Binary files a/tests/test.parquet and b/tests/test.parquet differ diff --git a/tests/users.test.sqlite b/tests/users.test.sqlite index 8c70b44..16de761 100644 Binary files a/tests/users.test.sqlite and b/tests/users.test.sqlite differ diff --git a/uv.lock b/uv.lock index 83a3f6e..110706c 100644 --- a/uv.lock +++ b/uv.lock @@ -66,6 +66,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "authlib" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, +] + [[package]] name = "backports-datetime-fromisoformat" version = "2.0.3" @@ -854,6 +867,7 @@ name = "decp-info" version = "2.8.0" source = { virtual = "." } dependencies = [ + { name = "authlib" }, { name = "boto3" }, { name = "brevo-python" }, { name = "cryptography" }, @@ -898,6 +912,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "authlib" }, { name = "boto3" }, { name = "brevo-python", specifier = "==5.0.0rc1" }, { name = "cryptography" }, @@ -1316,6 +1331,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "joserfc" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" }, +] + [[package]] name = "jsbeautifier" version = "1.15.4"