diff --git a/.env.example b/.env.example index c2669f96f..74fa58746 100644 --- a/.env.example +++ b/.env.example @@ -33,3 +33,6 @@ AUTHENTIK_URL= AUTHENTIK_CLIENT_ID= AUTHENTIK_AUTHORIZE_URL= AUTHENTIK_TOKEN_URL= + +# middleware +SESSION_SECRET_KEY=your_secret_key_here diff --git a/admin/auth.py b/admin/auth.py index d1097f588..5d08d450e 100644 --- a/admin/auth.py +++ b/admin/auth.py @@ -199,13 +199,7 @@ def get_admin_user(self, request: Request) -> Optional[AdminUser]: except Exception: return None - async def login( - self, - username: str, - password: str, - remember_me: bool, - request: Request, - ) -> RedirectResponse: + async def login(self, *args, **kwargs) -> RedirectResponse: """ Redirect to Authentik OIDC login page. @@ -213,14 +207,26 @@ async def login( and redirect to Authentik OAuth flow instead. Args: - username: Ignored (OIDC handles authentication) - password: Ignored (OIDC handles authentication) - remember_me: Ignored - request: Starlette request object + request: Starlette request object (extracted from args/kwargs) + *args/**kwargs: Ignored, kept for compatibility with different + Starlette Admin login call signatures Returns: RedirectResponse to Authentik authorization endpoint """ + # Starlette Admin has changed the AuthProvider.login signature across versions. + # Accept *args/**kwargs and extract the Request to stay compatible whether + # it calls login(request, data, ...) or login(username, password, remember_me, request). + request: Optional[Request] = kwargs.get("request") + if request is None: + for arg in args: + if isinstance(arg, Request): + request = arg + break + + if request is None: + raise LoginFailed("Unable to determine login request context.") + # Redirect to Authentik OAuth authorization endpoint authentik_authorize_url = os.environ.get("AUTHENTIK_AUTHORIZE_URL") if not authentik_authorize_url: diff --git a/admin/config.py b/admin/config.py index 6fc3f1676..88251c89b 100644 --- a/admin/config.py +++ b/admin/config.py @@ -58,7 +58,7 @@ def create_admin(app): # Create admin instance admin = Admin( engine=engine, - title="NM Sample Locations Admin", + title="Ocotillod Admin", base_url="/admin", logo_url=None, # TODO: Add NMBGMR logo auth_provider=NMSampleLocationsAuthProvider(), diff --git a/admin/fields.py b/admin/fields.py index 4299e4ea5..cd71bbc17 100644 --- a/admin/fields.py +++ b/admin/fields.py @@ -42,7 +42,7 @@ class WKTField(StringField): Note: Longitude comes first, then latitude (POINT(lon lat), not POINT(lat lon)) """ - def serialize_value(self, request: Request, value: Any, action: str) -> str: + async def serialize_value(self, request: Request, value: Any, action: str) -> str: """ Convert WKBElement (PostGIS geometry) to WKT string for display in form. diff --git a/admin/views/contact.py b/admin/views/contact.py index f28eede44..3a85de06b 100644 --- a/admin/views/contact.py +++ b/admin/views/contact.py @@ -201,7 +201,7 @@ def can_view_details(self, request: Request) -> bool: # ========== Data Visibility (Release Status Filter) ========== - async def get_list_query(self, request: Request): + def get_list_query(self, request: Request): query = select(self.model) user = getattr(request.state, "user", None) diff --git a/admin/views/deployment.py b/admin/views/deployment.py index d9b9031e7..3616a0894 100644 --- a/admin/views/deployment.py +++ b/admin/views/deployment.py @@ -202,7 +202,7 @@ def can_view_details(self, request: Request) -> bool: # ========== Data Visibility (Release Status Filter) ========== - async def get_list_query(self, request: Request): + def get_list_query(self, request: Request): query = select(self.model) user = getattr(request.state, "user", None) diff --git a/admin/views/location.py b/admin/views/location.py index e35149343..ad8663898 100644 --- a/admin/views/location.py +++ b/admin/views/location.py @@ -204,7 +204,7 @@ def can_view_details(self, request: Request) -> bool: # ========== Data Visibility (Release Status Filter) ========== - async def get_list_query(self, request: Request): + def get_list_query(self, request: Request): query = select(self.model) user = getattr(request.state, "user", None) diff --git a/admin/views/observation.py b/admin/views/observation.py index c1257f786..8a4c3437e 100644 --- a/admin/views/observation.py +++ b/admin/views/observation.py @@ -209,7 +209,7 @@ def can_view_details(self, request: Request) -> bool: # ========== Data Visibility (Release Status Filter) ========== - async def get_list_query(self, request: Request): + def get_list_query(self, request: Request): query = select(self.model) user = getattr(request.state, "user", None) diff --git a/admin/views/sensor.py b/admin/views/sensor.py index 73097c2c4..c57cf52a4 100644 --- a/admin/views/sensor.py +++ b/admin/views/sensor.py @@ -202,7 +202,7 @@ def can_view_details(self, request: Request) -> bool: # ========== Data Visibility (Release Status Filter) ========== - async def get_list_query(self, request: Request): + def get_list_query(self, request: Request): query = select(self.model) user = getattr(request.state, "user", None) diff --git a/admin/views/thing.py b/admin/views/thing.py index df4bbc60e..f9e155c36 100644 --- a/admin/views/thing.py +++ b/admin/views/thing.py @@ -254,7 +254,7 @@ def can_view_details(self, request: Request) -> bool: # ========== Data Visibility (Release Status Filter) ========== - async def get_list_query(self, request: Request): + def get_list_query(self, request: Request): query = select(self.model) user = getattr(request.state, "user", None) diff --git a/core/app.py b/core/app.py index b0e0184fe..a0acc2539 100644 --- a/core/app.py +++ b/core/app.py @@ -24,6 +24,7 @@ ) from fastapi.openapi.utils import get_openapi +from transfers.seed import seed_all from .initializers import ( register_routes, erase_and_rebuild_db, @@ -38,6 +39,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """ if settings.get_enum("MODE") == "development": erase_and_rebuild_db() + seed_all(10) register_routes(app) yield diff --git a/main.py b/main.py index 915a01720..f56a99e60 100644 --- a/main.py +++ b/main.py @@ -26,11 +26,19 @@ from starlette.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware from core.app import app register_routes(app) +# Session middleware is required for the admin auth flow (request.session access). +SESSION_SECRET_KEY = os.environ.get("SESSION_SECRET_KEY") +if not SESSION_SECRET_KEY: + raise ValueError("SESSION_SECRET_KEY environment variable is not set.") + +app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET_KEY) + # ========== Starlette Admin Interface ========== # Mount admin interface at /admin # This provides a web-based UI for managing database records (replaces MS Access) diff --git a/pyproject.toml b/pyproject.toml index 55458fd52..14f7e9690 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "numpy==2.3.3", "packaging==25.0", "pandas==2.3.2", - "pandas-stubs==2.3.0.250703", + "pandas-stubs~=2.3.2", "pg8000==1.31.5", "phonenumbers==9.0.13", "pillow==11.3.0", diff --git a/transfers/transfer.py b/transfers/transfer.py index 59d8bab71..d1b359bae 100644 --- a/transfers/transfer.py +++ b/transfers/transfer.py @@ -169,7 +169,7 @@ def transfer_all(metrics, limit=100): transfer_acoustic = get_bool_env("TRANSFER_WATERLEVELS_ACOUSTIC", True) transfer_link_ids = get_bool_env("TRANSFER_LINK_IDS", True) transfer_groups = get_bool_env("TRANSFER_GROUPS", True) - transfer_assets = get_bool_env("TRANSFER_ASSETS", True) + transfer_assets = get_bool_env("TRANSFER_ASSETS", False) use_parallel = get_bool_env("TRANSFER_PARALLEL", True) if use_parallel: diff --git a/uv.lock b/uv.lock index dd3eb4389..67ea6ae0d 100644 --- a/uv.lock +++ b/uv.lock @@ -1171,7 +1171,7 @@ requires-dist = [ { name = "numpy", specifier = "==2.3.3" }, { name = "packaging", specifier = "==25.0" }, { name = "pandas", specifier = "==2.3.2" }, - { name = "pandas-stubs", specifier = "==2.3.0.250703" }, + { name = "pandas-stubs", specifier = "~=2.3.2" }, { name = "pg8000", specifier = "==1.31.5" }, { name = "phonenumbers", specifier = "==9.0.13" }, { name = "pillow", specifier = "==11.3.0" }, @@ -1265,15 +1265,15 @@ wheels = [ [[package]] name = "pandas-stubs" -version = "2.3.0.250703" +version = "2.3.3.251219" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "types-pytz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/df/c1c51c5cec087b8f4d04669308b700e9648745a77cdd0c8c5e16520703ca/pandas_stubs-2.3.0.250703.tar.gz", hash = "sha256:fb6a8478327b16ed65c46b1541de74f5c5947f3601850caf3e885e0140584717", size = 103910, upload-time = "2025-07-02T17:49:11.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/ee/5407e9e63d22a47774f9246ca80b24f82c36f26efd39f9e3c5b584b915aa/pandas_stubs-2.3.3.251219.tar.gz", hash = "sha256:dc2883e6daff49d380d1b5a2e864983ab9be8cd9a661fa861e3dea37559a5af4", size = 106899, upload-time = "2025-12-19T15:49:53.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/cb/09d5f9bf7c8659af134ae0ffc1a349038a5d0ff93e45aedc225bde2872a3/pandas_stubs-2.3.0.250703-py3-none-any.whl", hash = "sha256:a9265fc69909f0f7a9cabc5f596d86c9d531499fed86b7838fd3278285d76b81", size = 154719, upload-time = "2025-07-02T17:49:10.697Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/69f2a39792a653fd64d916cd563ed79ec6e5dcfa6408c4674021d810afcf/pandas_stubs-2.3.3.251219-py3-none-any.whl", hash = "sha256:ccc6337febb51d6d8a08e4c96b479478a0da0ef704b5e08bd212423fe1cb549c", size = 163667, upload-time = "2025-12-19T15:49:52.072Z" }, ] [[package]]