diff --git a/fileglancer/app.py b/fileglancer/app.py
index b4e92bb4..cbacdb7d 100644
--- a/fileglancer/app.py
+++ b/fileglancer/app.py
@@ -212,11 +212,11 @@ def mask_password(url: str) -> str:
# Initialize database (run migrations once at startup)
db.initialize_database(settings.db_url)
- # Mount static assets (CSS, JS, images) at /fg/assets
+ # Mount static assets (CSS, JS, images) at /assets
assets_dir = ui_dir / "assets"
if assets_dir.exists():
- app.mount("/fg/assets", StaticFiles(directory=str(assets_dir)), name="assets")
- logger.debug(f"Mounted static assets at /fg/assets from {assets_dir}")
+ app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
+ logger.debug(f"Mounted static assets at /assets from {assets_dir}")
else:
logger.warning(f"Assets directory not found at {assets_dir}")
@@ -294,7 +294,7 @@ async def login(request: Request, next: Optional[str] = Query(None)):
raise HTTPException(status_code=404, detail="OKTA authentication not enabled")
# Store the next URL in the session for use after OAuth callback
- if next and next.startswith("/fg/"):
+ if next and next.startswith("/"):
request.session['next_url'] = next
redirect_uri = str(settings.okta_redirect_uri)
@@ -346,11 +346,11 @@ async def auth_callback(request: Request, response: Response):
session_id = user_session.session_id
# Get the next URL from session (stored during initial login redirect)
- next_url = request.session.pop('next_url', '/fg/browse')
+ next_url = request.session.pop('next_url', '/browse')
# Validate next_url to prevent open redirect vulnerabilities
- if not next_url.startswith('/fg/'):
- next_url = '/fg/browse'
+ if not next_url.startswith('/'):
+ next_url = '/browse'
# Create redirect response
redirect_response = RedirectResponse(url=next_url)
@@ -408,7 +408,7 @@ async def cli_login(request: Request, session_id: str):
username = user_session.username
# Create redirect response to browse page
- redirect_response = RedirectResponse(url="/fg/browse")
+ redirect_response = RedirectResponse(url="/browse")
# Set session cookie
auth.create_session_cookie(redirect_response, session_id, settings)
@@ -1144,7 +1144,7 @@ async def simple_login_handler(request: Request, body: dict = Body(...)):
# Parse JSON body
username = body.get("username")
- next_url = body.get("next", "/fg/browse")
+ next_url = body.get("next", "/browse")
if not username or not username.strip():
raise HTTPException(status_code=400, detail="Username is required")
@@ -1152,9 +1152,9 @@ async def simple_login_handler(request: Request, body: dict = Body(...)):
username = username.strip()
# Validate next_url to prevent open redirect vulnerabilities
- # Only allow relative URLs that start with /fg/
- if not next_url.startswith("/fg/"):
- next_url = "/fg/browse"
+ # Only allow relative URLs that start with /
+ if not next_url.startswith("/"):
+ next_url = "/browse"
# Create session in database
expires_at = datetime.now(UTC) + timedelta(hours=settings.session_expiry_hours)
@@ -1182,19 +1182,15 @@ async def simple_login_handler(request: Request, body: dict = Body(...)):
return response
- # Home page - redirect to /fg
- @app.get("/", include_in_schema=False)
- async def home_page():
- """Redirect root to /fg"""
- return RedirectResponse(url="/fg/")
-
-
- # Serve SPA at /fg/* for client-side routing
+ # Serve SPA at /* for client-side routing
# This must be the LAST route registered
- @app.get("/fg/{full_path:path}", include_in_schema=False)
- @app.get("/fg", include_in_schema=False)
+ @app.get("/{full_path:path}", include_in_schema=False)
async def serve_spa(full_path: str = ""):
- """Serve index.html for all SPA routes under /fg/ (client-side routing)"""
+ """Serve index.html for all SPA routes (client-side routing)"""
+ # Don't serve SPA for API or files paths - those should 404 if not found
+ if full_path and (full_path.startswith("api/") or full_path.startswith("files/")):
+ raise HTTPException(status_code=404, detail="Not found")
+
# append the full_path to the ui_dir and ensure it is within the ui_dir after resolving
resolved_dir = os.path.normpath(ui_dir / full_path)
# if the resolved_dir is outside of ui_dir, reject the request
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index c703a119..e62dafa7 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,12 +1,13 @@
import type { ReactNode } from 'react';
-import { BrowserRouter, Route, Routes } from 'react-router';
+import { useEffect } from 'react';
+import { BrowserRouter, Route, Routes, useNavigate } from 'react-router';
import { ErrorBoundary } from 'react-error-boundary';
import { AuthContextProvider, useAuthContext } from '@/contexts/AuthContext';
import { MainLayout } from './layouts/MainLayout';
import { BrowsePageLayout } from './layouts/BrowseLayout';
import { OtherPagesLayout } from './layouts/OtherPagesLayout';
-import Home from '@/components/Home';
+import Login from '@/components/Login';
import Browse from '@/components/Browse';
import Help from '@/components/Help';
import Jobs from '@/components/Jobs';
@@ -26,12 +27,12 @@ function RequireAuth({ children }: { readonly children: ReactNode }) {
);
}
- // If not authenticated, redirect to home page with the current URL as 'next' parameter
+ // If not authenticated, redirect to login page with the current URL as 'next' parameter
if (!authStatus?.authenticated) {
const currentPath =
window.location.pathname + window.location.search + window.location.hash;
const encodedNext = encodeURIComponent(currentPath);
- window.location.href = `/fg/?next=${encodedNext}`;
+ window.location.href = `/login?next=${encodedNext}`;
return (
Redirecting to login...
@@ -42,37 +43,53 @@ function RequireAuth({ children }: { readonly children: ReactNode }) {
return children;
}
-function getBasename() {
- const { pathname } = window.location;
- // Try to match /user/:username/lab
- const userLabMatch = pathname.match(/^\/user\/[^/]+\/fg/);
- if (userLabMatch) {
- // Return the matched part, e.g. "/user//lab"
- return userLabMatch[0];
- }
- // Otherwise, check if it starts with /lab
- if (pathname.startsWith('/fg')) {
- return '/fg';
- }
- // Fallback to root if no match is found
- return '/fg';
-}
-
/**
- * React component for a counter.
- *
- * @returns The React component
+ * Root redirect component that handles smart routing based on auth status
+ * This component serves as a safe landing page after login to allow
+ * auth queries to update before navigating to protected routes
*/
+function RootRedirect() {
+ const { loading, authStatus } = useAuthContext();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (loading) {
+ return;
+ }
+
+ const urlParams = new URLSearchParams(window.location.search);
+ const nextUrl = urlParams.get('next');
+
+ if (authStatus?.authenticated) {
+ // User is authenticated - navigate to next URL or default to /browse
+ const destination =
+ nextUrl && nextUrl.startsWith('/') ? nextUrl : '/browse';
+ navigate(destination, { replace: true });
+ } else {
+ // User is not authenticated - redirect to login
+ const encodedNext = nextUrl ? `?next=${encodeURIComponent(nextUrl)}` : '';
+ navigate(`/login${encodedNext}`, { replace: true });
+ }
+ }, [loading, authStatus, navigate]);
+
+ // Show loading state while determining where to route
+ return (
+