diff --git a/__mocks__/handlers.ts b/__mocks__/handlers.ts index 7ccd151..d503253 100644 --- a/__mocks__/handlers.ts +++ b/__mocks__/handlers.ts @@ -37,7 +37,8 @@ const mockSession = { image: null, }, scopes: ['dataset:update', 'stac:collection:update'], - tenants: ['tenant1', 'tenant2', 'tenant3'], + allowedTenants: ['tenant1', 'tenant2', 'tenant3'], + accessToken: 'mock-access-token', expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), }; @@ -178,6 +179,12 @@ export const handlers = [ return HttpResponse.json(mockSession); }), + http.get('/api/allowed-tenants', ({ request }) => { + return HttpResponse.json({ + tenants: ['tenant1', 'tenant2', 'tenant3'], + }); + }), + http.get( 'https://example.com/api/raster/cog/tiles/WebMercatorQuad/:z/:x/:y.png', ({ request, params }) => { diff --git a/__tests__/pages/CreateIngestPageClient.test.tsx b/__tests__/pages/CreateIngestPageClient.test.tsx index d90fa65..421fca7 100644 --- a/__tests__/pages/CreateIngestPageClient.test.tsx +++ b/__tests__/pages/CreateIngestPageClient.test.tsx @@ -13,9 +13,7 @@ vi.mock('@/components/layout/AppLayout', () => ({ })); const AllProviders = ({ children }: { children: ReactNode }) => { - const mockSession = { - expires: '1', - }; + const mockSession = null; const mockTenantContext = { allowedTenants: ['test-tenant-1', 'test-tenant-2'], @@ -23,11 +21,9 @@ const AllProviders = ({ children }: { children: ReactNode }) => { }; return ( - - - {children} - - + + {children} + ); }; diff --git a/app/contexts/TenantContext.tsx b/app/contexts/TenantContext.tsx index 5162638..3336065 100644 --- a/app/contexts/TenantContext.tsx +++ b/app/contexts/TenantContext.tsx @@ -1,6 +1,12 @@ 'use client'; -import React, { createContext, useContext, ReactNode } from 'react'; +import React, { + createContext, + useContext, + ReactNode, + useEffect, + useState, +} from 'react'; import { Spin } from 'antd'; import { useSession } from 'next-auth/react'; @@ -15,10 +21,28 @@ export const TenantContext = createContext( export const TenantProvider = ({ children }: { children: ReactNode }) => { const { data: session, status } = useSession(); + const [allowedTenants, setAllowedTenants] = useState([]); + const [isLoading, setIsLoading] = useState(true); - const isLoading = status === 'loading'; + useEffect(() => { + if (status === 'loading') { + setIsLoading(true); + return; + } - const allowedTenants = session?.tenants || []; + setIsLoading(false); + + if (!session) { + setAllowedTenants([]); + return; + } + + const sessionAllowedTenants = (session as any)?.allowedTenants; + const tenants = Array.isArray(sessionAllowedTenants) + ? sessionAllowedTenants + : []; + setAllowedTenants(tenants); + }, [session, status]); if (isLoading) { return ; diff --git a/auth.ts b/auth.ts index 7c73883..34bb276 100644 --- a/auth.ts +++ b/auth.ts @@ -2,6 +2,7 @@ import NextAuth, { type NextAuthConfig, Session } from 'next-auth'; import KeycloakProvider from 'next-auth/providers/keycloak'; import { JWT } from 'next-auth/jwt'; import { NextResponse } from 'next/server'; +import { VEDA_BACKEND_URL } from '@/config/env'; const authDisabled = process.env.NEXT_PUBLIC_DISABLE_AUTH === 'true'; @@ -14,10 +15,21 @@ const getMockTenants = (): string[] => { .map((tenant) => tenant.trim()) .filter(Boolean); } - // Default fallback tenants if none specified return ['']; }; +const getMockScopes = (): string[] => { + const mockScopes = process.env.NEXT_PUBLIC_MOCK_SCOPES; + if (mockScopes && mockScopes.trim() !== '') { + // Handle both comma and space separated scopes + return mockScopes + .split(/[,\s]+/) + .map((scope) => scope.trim()) + .filter(Boolean); + } + return []; +}; + let auth: any, handlers: any, signIn: any, signOut: any; if (authDisabled) { @@ -27,23 +39,21 @@ if (authDisabled) { const mockTenants = getMockTenants(); console.log('🎭 Mock tenants:', mockTenants); - // Inject mock scopes from env if present - let mockScopes: string[] = []; - if ( - process.env.NEXT_PUBLIC_MOCK_SCOPES && - process.env.NEXT_PUBLIC_MOCK_SCOPES.trim() !== '' - ) { - mockScopes = - process.env.NEXT_PUBLIC_MOCK_SCOPES.split(/[ ,]+/).filter(Boolean); - console.log('🎭 Mock scopes:', mockScopes); - } - const mockSession: Session & { scopes?: string[] } = { + const mockScopes = getMockScopes(); + console.log('Mock scopes:', mockScopes); + + const mockSession: Session & { + scopes?: string[]; + accessToken?: string; + allowedTenants?: string[]; + } = { user: { name: 'Mock User', email: 'test@example.com', }, expires: '2099-12-31T23:59:59.999Z', - tenants: mockTenants, + allowedTenants: mockTenants, + accessToken: 'mock-access-token-for-development', ...(mockScopes.length > 0 ? { scopes: mockScopes } : {}), }; @@ -93,6 +103,52 @@ if (authDisabled) { } else if (typeof rawScopes === 'string') { (token as JWT).scopes = rawScopes.split(' '); } + + try { + if (process.env.NEXT_PUBLIC_DISABLE_AUTH === 'true') { + console.log( + 'Skipping external tenants fetch in test environment' + ); + const mockTenants = process.env.NEXT_PUBLIC_MOCK_TENANTS; + if (mockTenants && mockTenants.trim() !== '') { + (token as JWT).allowedTenants = mockTenants + .split(',') + .map((tenant) => tenant.trim()) + .filter(Boolean); + } + } else { + const allowedTenantsResponse = await fetch( + `${VEDA_BACKEND_URL}/ingest/auth/tenants/writable`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${account.access_token}`, + Accept: 'application/json', + }, + } + ); + console.log({ allowedTenantsResponse }); + + if (allowedTenantsResponse.ok) { + const allowedTenantsData = await allowedTenantsResponse.json(); + console.log( + 'Fetched allowed tenants during auth:', + allowedTenantsData.tenants + ); + (token as JWT).allowedTenants = + allowedTenantsData.tenants || []; + } else { + console.warn( + 'Failed to fetch allowed tenants during auth:', + allowedTenantsResponse.status + ); + (token as JWT).allowedTenants = []; + } + } + } catch (error) { + console.error('Error fetching allowed tenants during auth:', error); + (token as JWT).allowedTenants = []; + } } return token; }, @@ -101,8 +157,18 @@ if (authDisabled) { const customSession = session as Session & { tenants?: string[]; scopes?: string[]; + accessToken?: string; + allowedTenants?: string[]; }; + if (customToken.accessToken) { + (customSession as any).accessToken = customToken.accessToken; + } + + if (customToken.allowedTenants) { + customSession.allowedTenants = customToken.allowedTenants as string[]; + } + // Check if we should use mock tenants instead of real ones const mockTenants = process.env.NEXT_PUBLIC_MOCK_TENANTS; if (mockTenants && mockTenants.trim() !== '') { diff --git a/middleware.ts b/middleware.ts index 8417cb2..0493925 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,22 +6,26 @@ const DISABLE_AUTH = process.env.NEXT_PUBLIC_DISABLE_AUTH === 'true'; // Define route permissions in a declarative way const routeConfig = { // Routes that require authentication but no special permissions - limited: ['/collections', '/datasets', '/cog-viewer'], + limited: ['/collections', '/datasets', '/cog-viewer', '/upload-url'], // Routes that require create permissions (blocked for limited access) - createAccess: ['/create-collection', '/create-dataset', '/upload'], + createAccess: [ + '/create-collection', + '/create-dataset', + '/upload', + '/create-ingest', + ], // Routes that require edit permissions (blocked for limited access + need dataset:update) - editAccess: ['/edit-collection', '/edit-dataset'], - - editStacCollectionAccess: ['/edit-existing-collection'], - - // API routes that require authentication - apiRoutes: [ + editAccess: [ + '/edit-collection', + '/edit-dataset', '/list-ingests', '/retrieve-ingest', - '/create-ingest', - '/upload-url', + ], + + editStacCollectionAccess: [ + '/edit-existing-collection', '/existing-collection', ], }; @@ -60,8 +64,8 @@ function isRouteAllowed(pathname: string, permissionLevel: string) { return false; case 'limited': - // Limited users can access authenticated routes, but not create/edit - return matchesRoute(routeConfig.limited); + // Limited users can access authenticated routes and upload-url, but not create/edit + return matchesRoute([...routeConfig.limited]); case 'create': return matchesRoute([ @@ -106,7 +110,9 @@ export async function middleware(request: NextRequest) { } if (DISABLE_AUTH) { - console.warn('WARNING: Authentication is disabled for development'); + console.warn( + 'WARNING: Authentication is disabled for development - middleware skipping auth checks' + ); return NextResponse.next(); }