Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ AUTHENTIK_URL=
AUTHENTIK_CLIENT_ID=
AUTHENTIK_AUTHORIZE_URL=
AUTHENTIK_TOKEN_URL=

# middleware
SESSION_SECRET_KEY=your_secret_key_here
28 changes: 17 additions & 11 deletions admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,28 +199,34 @@ 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.

Note: Starlette Admin will show a login form, but we ignore the username/password
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:
Expand Down
2 changes: 1 addition & 1 deletion admin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion admin/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion admin/views/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion admin/views/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion admin/views/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion admin/views/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion admin/views/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion admin/views/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion transfers/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading