Skip to content

Commit 0e299ec

Browse files
authored
Merge pull request #34 from benavlabs/root-mount-path
add support for root mount
2 parents 1dc43af + 7ba1bd2 commit 0e299ec

File tree

6 files changed

+282
-25
lines changed

6 files changed

+282
-25
lines changed

crudadmin/admin_interface/admin_site.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ def __init__(
144144

145145
self.secure_cookies: bool = secure_cookies
146146

147+
def get_url_prefix(self) -> str:
148+
"""Get the URL prefix for admin routes, handling root mount path correctly."""
149+
return f"/{self.mount_path}" if self.mount_path else ""
150+
147151
def setup_routes(self) -> None:
148152
"""
149153
Configure all admin interface routes including auth, dashboard and model views.
@@ -270,16 +274,17 @@ async def login_page_inner(
270274

271275
logger.info(f"Session created successfully: {session_id}")
272276

273-
response = RedirectResponse(
274-
url=f"/{self.mount_path}/", status_code=303
277+
dashboard_url = (
278+
f"{self.get_url_prefix()}/" if self.mount_path else "/"
275279
)
280+
response = RedirectResponse(url=dashboard_url, status_code=303)
276281

277282
self.session_manager.set_session_cookies(
278283
response=response,
279284
session_id=session_id,
280285
csrf_token=csrf_token,
281286
secure=self.secure_cookies,
282-
path=f"/{self.mount_path}",
287+
path=f"{self.get_url_prefix()}/" if self.mount_path else "/",
283288
)
284289

285290
await db.commit()
@@ -340,13 +345,12 @@ async def logout_endpoint_inner(
340345
if session_id:
341346
await self.session_manager.terminate_session(session_id=session_id)
342347

343-
response = RedirectResponse(
344-
url=f"/{self.mount_path}/login", status_code=303
345-
)
348+
login_url = f"{self.get_url_prefix()}/login"
349+
response = RedirectResponse(url=login_url, status_code=303)
346350

347351
self.session_manager.clear_session_cookies(
348352
response=response,
349-
path=f"/{self.mount_path}",
353+
path=f"{self.get_url_prefix()}/" if self.mount_path else "/",
350354
)
351355

352356
return response
@@ -380,9 +384,10 @@ async def admin_login_page_inner(
380384
)
381385

382386
if is_valid_session:
383-
return RedirectResponse(
384-
url=f"/{self.mount_path}/", status_code=303
387+
dashboard_url = (
388+
f"{self.get_url_prefix()}/" if self.mount_path else "/"
385389
)
390+
return RedirectResponse(url=dashboard_url, status_code=303)
386391

387392
except Exception:
388393
pass

crudadmin/admin_interface/crud_admin.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,12 @@ def __init__(
318318
track_events: bool = False,
319319
track_sessions_in_db: bool = False,
320320
) -> None:
321-
self.mount_path = mount_path.strip("/") if mount_path else "admin"
321+
if mount_path == "/":
322+
self.mount_path = ""
323+
elif mount_path:
324+
self.mount_path = mount_path.strip("/")
325+
else:
326+
self.mount_path = "admin"
322327
self.theme = theme or "dark-theme"
323328
self.track_events = track_events
324329
self.track_sessions_in_db = track_sessions_in_db
@@ -376,7 +381,9 @@ def __init__(
376381
self.initial_admin = initial_admin
377382
self.models: Dict[str, ModelConfig] = {}
378383
self.router = APIRouter(tags=["admin"])
379-
self.oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"/{self.mount_path}/login")
384+
self.oauth2_scheme = OAuth2PasswordBearer(
385+
tokenUrl=f"{self.get_url_prefix()}/login"
386+
)
380387
self.secure_cookies = secure_cookies
381388

382389
session_backend = getattr(self, "_session_backend", "memory")
@@ -434,6 +441,10 @@ def __init__(
434441

435442
self.app.include_router(self.router)
436443

444+
def get_url_prefix(self) -> str:
445+
"""Get the URL prefix for admin routes, handling root mount path correctly."""
446+
return f"/{self.mount_path}" if self.mount_path else ""
447+
437448
async def initialize(self) -> None:
438449
"""
439450
Initialize admin database tables and create initial admin user.

crudadmin/admin_interface/middleware/auth.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ def _should_add_cache_headers(self, response: Response) -> bool:
3838
return not (300 <= response.status_code < 400)
3939

4040
async def dispatch(self, request: Request, call_next):
41-
if not request.url.path.startswith(f"/{self.admin_instance.mount_path}/"):
41+
expected_prefix = (
42+
f"/{self.admin_instance.mount_path}/"
43+
if self.admin_instance.mount_path
44+
else "/"
45+
)
46+
if not request.url.path.startswith(expected_prefix):
4247
return await call_next(request)
4348

4449
is_login_path = request.url.path.endswith("/login")
@@ -58,8 +63,9 @@ async def dispatch(self, request: Request, call_next):
5863

5964
if not session_id:
6065
logger.debug("Missing session_id")
66+
login_url = f"{self.admin_instance.get_url_prefix()}/login?error=Please+log+in+to+access+this+page"
6167
return RedirectResponse(
62-
url=f"/{self.admin_instance.mount_path}/login?error=Please+log+in+to+access+this+page",
68+
url=login_url,
6369
status_code=303,
6470
)
6571

@@ -72,8 +78,9 @@ async def dispatch(self, request: Request, call_next):
7278

7379
if not session_data:
7480
logger.debug("Invalid or expired session")
81+
login_url = f"{self.admin_instance.get_url_prefix()}/login?error=Session+expired"
7582
return RedirectResponse(
76-
url=f"/{self.admin_instance.mount_path}/login?error=Session+expired",
83+
url=login_url,
7784
status_code=303,
7885
)
7986

@@ -84,8 +91,9 @@ async def dispatch(self, request: Request, call_next):
8491

8592
if not user:
8693
logger.debug("User not found for session")
94+
login_url = f"{self.admin_instance.get_url_prefix()}/login?error=User+not+found"
8795
return RedirectResponse(
88-
url=f"/{self.admin_instance.mount_path}/login?error=User+not+found",
96+
url=login_url,
8997
status_code=303,
9098
)
9199

@@ -107,8 +115,9 @@ async def dispatch(self, request: Request, call_next):
107115
or "/crud/" in request.url.path
108116
):
109117
raise
118+
login_url = f"{self.admin_instance.get_url_prefix()}/login?error=Authentication+error"
110119
return RedirectResponse(
111-
url=f"/{self.admin_instance.mount_path}/login?error=Authentication+error",
120+
url=login_url,
112121
status_code=303,
113122
)
114123

crudadmin/admin_interface/model_view.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,14 @@ def __init__(
423423

424424
self.setup_routes()
425425

426+
def get_url_prefix(self) -> str:
427+
"""Get the URL prefix for admin routes, handling root mount path correctly."""
428+
if self.admin_site:
429+
return (
430+
f"/{self.admin_site.mount_path}" if self.admin_site.mount_path else ""
431+
)
432+
return ""
433+
426434
def _model_is_admin_model(self, model: Type[DeclarativeBase]) -> bool:
427435
"""Check if the given model is one of the admin-specific models."""
428436
admin_model_names = [
@@ -638,15 +646,16 @@ async def form_create_endpoint_inner(
638646

639647
if result:
640648
request.state.crud_result = result
649+
model_list_url = (
650+
f"{self.get_url_prefix()}/{self.model.__name__}/"
651+
)
641652
if "HX-Request" in request.headers:
642653
return RedirectResponse(
643-
url=f"/{self.admin_site.mount_path}/{self.model.__name__}/",
644-
headers={
645-
"HX-Redirect": f"/{self.admin_site.mount_path}/{self.model.__name__}/"
646-
},
654+
url=model_list_url,
655+
headers={"HX-Redirect": model_list_url},
647656
)
648657
return RedirectResponse(
649-
url=f"/{self.admin_site.mount_path}/{self.model.__name__}/",
658+
url=model_list_url,
650659
status_code=303,
651660
)
652661

@@ -668,7 +677,7 @@ async def form_create_endpoint_inner(
668677
"error": error_message,
669678
"field_errors": field_errors,
670679
"field_values": field_values,
671-
"mount_path": self.admin_site.mount_path,
680+
"mount_path": self.admin_site.mount_path if self.admin_site else "",
672681
}
673682

674683
return self.templates.TemplateResponse(
@@ -827,7 +836,7 @@ async def bulk_delete_endpoint_inner(
827836
"current_page": adjusted_page,
828837
"rows_per_page": rows_per_page,
829838
"primary_key_info": primary_key_info,
830-
"mount_path": self.admin_site.mount_path,
839+
"mount_path": self.admin_site.mount_path if self.admin_site else "",
831840
}
832841

833842
return self.templates.TemplateResponse(
@@ -1204,8 +1213,11 @@ async def form_update_endpoint_inner(
12041213
)
12051214
await db.commit()
12061215

1216+
model_list_url = (
1217+
f"{self.get_url_prefix()}/{self.model.__name__}/"
1218+
)
12071219
return RedirectResponse(
1208-
url=f"/{self.admin_site.mount_path}/{self.model.__name__}/",
1220+
url=model_list_url,
12091221
status_code=303,
12101222
)
12111223

@@ -1232,7 +1244,7 @@ async def form_update_endpoint_inner(
12321244
"error": error_message,
12331245
"field_errors": field_errors,
12341246
"field_values": field_values,
1235-
"mount_path": self.admin_site.mount_path,
1247+
"mount_path": self.admin_site.mount_path if self.admin_site else "",
12361248
"id": id,
12371249
"include_sidebar_and_header": False,
12381250
}

tests/crud/test_admin.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,30 @@ async def test_crud_admin_with_custom_settings(async_session):
9898
assert admin.track_events is True
9999

100100

101+
@pytest.mark.asyncio
102+
async def test_crud_admin_root_mount_path(async_session):
103+
"""Test CRUDAdmin initialization with root mount path."""
104+
secret_key = "test-secret-key-for-testing-only-32-chars"
105+
db_config = create_test_db_config(async_session)
106+
107+
admin = CRUDAdmin(
108+
session=async_session,
109+
SECRET_KEY=secret_key,
110+
mount_path="/",
111+
db_config=db_config,
112+
setup_on_initialization=False,
113+
)
114+
115+
# Test that mount_path is properly set to empty string for root
116+
assert admin.mount_path == ""
117+
118+
# Test that URL prefix is correctly generated
119+
assert admin.get_url_prefix() == ""
120+
121+
# Test that OAuth2 token URL is correctly set for root path
122+
assert admin.oauth2_scheme.model.flows.password.tokenUrl == "/login"
123+
124+
101125
@pytest.mark.asyncio
102126
async def test_crud_admin_with_allowed_ips(async_session):
103127
"""Test CRUDAdmin initialization with IP restrictions."""

0 commit comments

Comments
 (0)