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
42 changes: 19 additions & 23 deletions fileglancer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1144,17 +1144,17 @@ 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")

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)
Expand Down Expand Up @@ -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
Expand Down
69 changes: 43 additions & 26 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<div className="flex h-screen items-center justify-center">
<div className="text-foreground">Redirecting to login...</div>
Expand All @@ -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/<username>/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 (
<div className="flex h-screen items-center justify-center">
<div className="text-foreground">Loading...</div>
</div>
);
}

const AppComponent = () => {
const basename = getBasename();
const tasksEnabled = import.meta.env.VITE_ENABLE_TASKS === 'true';

return (
<BrowserRouter basename={basename}>
<BrowserRouter>
<Routes>
<Route element={<MainLayout />} path="/*">
<Route element={<OtherPagesLayout />}>
<Route element={<Home />} index />
<Route element={<RootRedirect />} index />
<Route element={<Login />} path="login" />
<Route
element={
<RequireAuth>
Expand Down
Loading