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();
}