From be291e8e3749010b84f9ef988adcdd7ecd8b24cb Mon Sep 17 00:00:00 2001 From: Luiz Almeida Date: Fri, 11 Oct 2024 09:35:38 -0400 Subject: [PATCH 1/4] login and css fixes --- webui/src/App.css | 17 +-- webui/src/App.js | 138 ++++++++++----------- webui/src/api/api.js | 1 + webui/src/components/CollectionBrowser.css | 4 +- webui/src/components/CollectionBrowser.js | 16 ++- webui/src/components/CollectionForm.js | 2 +- webui/src/components/Collections.js | 1 + webui/src/components/Dashboard.css | 4 +- webui/src/components/Dashboard.js | 60 +++++---- webui/src/components/DocumentViewer.css | 4 +- webui/src/components/DocumentViewer.js | 5 +- webui/src/components/Footer.css | 8 +- webui/src/components/Header.js | 105 ++++++++++------ webui/src/components/SearchInterface.css | 3 - webui/src/components/SearchInterface.js | 4 +- webui/src/components/SideNav.js | 14 ++- webui/src/config/config.js | 3 +- webui/src/contexts/AuthContext.js | 97 +++++++-------- webui/src/services/authService.js | 24 +--- webui/src/styles/global.css | 8 +- 20 files changed, 260 insertions(+), 258 deletions(-) diff --git a/webui/src/App.css b/webui/src/App.css index 4c67a141..5b4cd17f 100644 --- a/webui/src/App.css +++ b/webui/src/App.css @@ -4,18 +4,19 @@ min-height: 100vh; } -.bx--content { - margin-top: 3rem; /* Adjust based on your Header height */ - flex-grow: 1; - background-color: #f4f4f4; -} - .page-container { - max-width: 1200px; - margin: 0 auto; + display: flex; + flex-direction: column; + flex-grow: 1; padding: 2rem; } +/* .bx--content { + margin-top: 3rem; Adjust based on your Header height + flex-grow: 1; + background-color: #f4f4f4; +} */ + @media (max-width: 1056px) { .page-container { padding: 1rem; diff --git a/webui/src/App.js b/webui/src/App.js index b59d1b2f..597d2592 100644 --- a/webui/src/App.js +++ b/webui/src/App.js @@ -1,75 +1,40 @@ -import React, { useEffect, useState } from 'react'; -import { BrowserRouter as Router, Route, Routes, Navigate, useNavigate, useLocation } from 'react-router-dom'; -import { Content } from 'carbon-components-react'; -import { AuthProvider, useAuth } from './contexts/AuthContext'; -import { NotificationProvider } from './contexts/NotificationContext'; -import Header from './components/Header'; -import Footer from './components/Footer'; -import Dashboard from './components/Dashboard'; -import SearchInterface from './components/SearchInterface'; -import CollectionBrowser from './components/CollectionBrowser'; -import DocumentViewer from './components/DocumentViewer'; -import LoginPage from './components/LoginPage'; -import { handleAuthCallback } from './services/authService'; -import config from './config/config'; -import './App.css'; -import './styles/global.css'; - -console.log('App.js loaded', { config }); - -const ProtectedRoute = ({ children }) => { - const { user, loading } = useAuth(); - const location = useLocation(); - - console.log('ProtectedRoute - user:', user, 'loading:', loading, 'location:', location); - - if (loading) { - return
Loading...
; - } - - if (!user) { - console.log('ProtectedRoute - Redirecting to login'); - return ; - } - - return children; -}; - -const AppLayout = ({ children }) => { - const { user } = useAuth(); - - console.log('AppLayout - user:', user); - - return ( -
- {user &&
} - -
- {children} -
-
- {user &&
} -
- ); -}; +import React, { useEffect } from "react"; +import { + BrowserRouter as Router, + Route, + Routes, + Navigate, + useNavigate, + useLocation, +} from "react-router-dom"; +import { Content } from "carbon-components-react"; +import { AuthProvider, useAuth } from "./contexts/AuthContext"; +import { NotificationProvider } from "./contexts/NotificationContext"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import Dashboard from "./components/Dashboard"; +import SearchInterface from "./components/SearchInterface"; +import CollectionBrowser from "./components/CollectionBrowser"; +import DocumentViewer from "./components/DocumentViewer"; +import LoginPage from "./components/LoginPage"; +import { handleAuthCallback } from "./services/authService"; +import Collections from "./components/Collections"; + +import "./App.css"; +import "./styles/global.css"; function AuthCallback() { const navigate = useNavigate(); const { fetchUser } = useAuth(); - console.log('AuthCallback component rendered'); - useEffect(() => { const processCallback = async () => { - console.log('Processing auth callback'); - const result = handleAuthCallback(); + const result = await handleAuthCallback(); if (result.success) { - console.log('Auth callback successful, fetching user'); await fetchUser(); - navigate('/'); + navigate("/"); } else { - console.log('Auth callback failed, redirecting to login'); - navigate('/login'); + navigate("/login"); } }; @@ -79,23 +44,40 @@ function AuthCallback() { return
Processing authentication...
; } -function AppContent() { - const { user, loading, fetchUser } = useAuth(); - const location = useLocation(); - - console.log('AppContent - user:', user, 'loading:', loading, 'location:', location); +const AppLayout = ({ children }) => { + const { user, fetchUser } = useAuth(); useEffect(() => { - if (!user && !loading) { - console.log('AppContent - Fetching user'); + if (!user) { fetchUser(); } - }, [user, loading, fetchUser]); + }, [user, fetchUser]); + + return ( +
+ {user &&
} + {children} + {user &&
} +
+ ); +}; + +const ProtectedRoute = ({ children }) => { + const { user, loading } = useAuth(); + const location = useLocation(); if (loading) { return
Loading...
; } + if (!user) { + return ; + } + + return children; +}; + +function AppContent() { return ( } /> @@ -117,13 +99,21 @@ function AppContent() { } /> } /> + + + + } + /> { return ( @@ -151,6 +139,6 @@ function App() { ); -} +}; export default App; diff --git a/webui/src/api/api.js b/webui/src/api/api.js index ef26551b..6084903b 100644 --- a/webui/src/api/api.js +++ b/webui/src/api/api.js @@ -100,6 +100,7 @@ export const getUserCollections = async () => { const response = await api.get(url); console.log('User collections fetched successfully', response.data); return response.data; + } catch (error) { console.error('Error in getUserCollections:', error); throw handleApiError(error, 'Error fetching user collections'); diff --git a/webui/src/components/CollectionBrowser.css b/webui/src/components/CollectionBrowser.css index 32b2853e..aeda608a 100644 --- a/webui/src/components/CollectionBrowser.css +++ b/webui/src/components/CollectionBrowser.css @@ -1,7 +1,5 @@ .collection-browser { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; + } .collection-actions { diff --git a/webui/src/components/CollectionBrowser.js b/webui/src/components/CollectionBrowser.js index 7208eb04..8f225607 100644 --- a/webui/src/components/CollectionBrowser.js +++ b/webui/src/components/CollectionBrowser.js @@ -199,7 +199,7 @@ const CollectionBrowser = () => { { key: 'actions', header: 'Actions' }, ]; - const collectionRows = collections.map((collection) => ({ + const collectionRows = collections?.map((collection) => ({ id: collection.id, name: collection.name, description: collection.description, @@ -235,7 +235,7 @@ const CollectionBrowser = () => { ), })); - const documentRows = documents.map((document) => ({ + const documentRows = documents?.map((document) => ({ id: document.id, name: document.name, type: document.type, @@ -262,8 +262,9 @@ const CollectionBrowser = () => { })); return ( -
-

{selectedCollection ? `Documents in ${selectedCollection.name}` : 'Document Collections'}

+ +
+

{selectedCollection ? `Documents in ${selectedCollection.name}` : 'Document Collections'}

{ ) : ( <> - + {({ rows, headers, getHeaderProps, getTableProps }) => ( @@ -389,8 +390,11 @@ const CollectionBrowser = () => {

Select a collection to move the document to:

c.id !== selectedCollection.id)} + items={collections ? collections.filter((c) => c.id !== selectedCollection.id) : []} itemToString={(item) => (item ? item.name : '')} onChange={({ selectedItem }) => setTargetCollection(selectedItem)} /> diff --git a/webui/src/components/CollectionForm.js b/webui/src/components/CollectionForm.js index 0faaadae..ce76ff22 100644 --- a/webui/src/components/CollectionForm.js +++ b/webui/src/components/CollectionForm.js @@ -144,7 +144,7 @@ const CollectionForm = () => { } return ( -
+

Your Collections

{isLoadingCollections ? ( diff --git a/webui/src/components/Collections.js b/webui/src/components/Collections.js index a037675a..5608e58e 100644 --- a/webui/src/components/Collections.js +++ b/webui/src/components/Collections.js @@ -63,6 +63,7 @@ const Collections = () => { setIsLoadingCollections(true); try { const collections = (await getUserCollections())?.collections; + console.log(collections); // setUserCollections(Array.isArray(collections) ? collections : []); // datatable requires a field called id diff --git a/webui/src/components/Dashboard.css b/webui/src/components/Dashboard.css index ada5bb6c..a8b7b8f0 100644 --- a/webui/src/components/Dashboard.css +++ b/webui/src/components/Dashboard.css @@ -1,7 +1,5 @@ .dashboard { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; + } .dashboard-content { diff --git a/webui/src/components/Dashboard.js b/webui/src/components/Dashboard.js index 030f9e04..4027999b 100644 --- a/webui/src/components/Dashboard.js +++ b/webui/src/components/Dashboard.js @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; import { Tile, ClickableTile, @@ -9,12 +9,12 @@ import { StructuredListBody, StructuredListRow, StructuredListCell, - Button -} from 'carbon-components-react'; -import { Add, Search, Document } from '@carbon/icons-react'; -import { getUserCollections } from '../api/api'; -import { useNotification } from '../contexts/NotificationContext'; -import './Dashboard.css'; + Button, +} from "carbon-components-react"; +import { Add, Search, Document } from "@carbon/icons-react"; +import { getUserCollections } from "../api/api"; +import { useNotification } from "../contexts/NotificationContext"; +import "./Dashboard.css"; const Dashboard = () => { const [collections, setCollections] = useState([]); @@ -28,12 +28,16 @@ const Dashboard = () => { const fetchDashboardData = async () => { setLoading(true); try { - const collectionsData = await getUserCollections(1, 5); - console.log('Fetched collections data:', collectionsData); - setCollections(collectionsData.data || []); + const collectionsData = await getUserCollections(); + // console.log("Fetched collections data:", collectionsData.collections); + setCollections(collectionsData.collections); } catch (error) { - console.error('Error fetching dashboard data:', error); - addNotification('error', 'Error', 'Failed to load dashboard data. Please try again later.'); + console.error("Error fetching dashboard data:", error); + addNotification( + "error", + "Error", + "Failed to load dashboard data. Please try again later." + ); setCollections([]); } finally { setLoading(false); @@ -44,7 +48,7 @@ const Dashboard = () => { return ; } - if (collections.length === 0) { + if (collections?.length === 0) { return (

Welcome to IBM RAG Solution

@@ -54,31 +58,31 @@ const Dashboard = () => { } return ( -
+

Welcome to IBM RAG Solution

-

Quick Actions

+

Quick Actions

-

Search Documents

+

Search Documents

Search across all your collections

-

Create Collection

+

Create Collection

Add a new document collection

-

Upload Document

+

Upload Document

Add a new document to a collection

-

Recent Collections

+

Recent Collections

@@ -88,19 +92,25 @@ const Dashboard = () => { - {collections.map((collection) => ( + {collections?.map((collection) => ( - {collection.name} + + {collection.name} + + + + {collection.documentCount} + + + {new Date(collection.lastUpdated).toLocaleDateString()} - {collection.documentCount} - {new Date(collection.lastUpdated).toLocaleDateString()} ))} - +
diff --git a/webui/src/components/DocumentViewer.css b/webui/src/components/DocumentViewer.css index f560bb61..1cc15af2 100644 --- a/webui/src/components/DocumentViewer.css +++ b/webui/src/components/DocumentViewer.css @@ -1,7 +1,5 @@ .document-viewer { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; + } .document-header { diff --git a/webui/src/components/DocumentViewer.js b/webui/src/components/DocumentViewer.js index 8c3b3970..bbf09d69 100644 --- a/webui/src/components/DocumentViewer.js +++ b/webui/src/components/DocumentViewer.js @@ -129,7 +129,7 @@ const DocumentViewer = () => { } return ( -
+
Home Collections @@ -140,7 +140,8 @@ const DocumentViewer = () => {
-

{document.title}

+

{document.title}

+
+ + ) : ( + Sign In + )} +
+ )} - - + + + ); }; diff --git a/webui/src/components/SearchInterface.css b/webui/src/components/SearchInterface.css index c41ada9f..c2470f5c 100644 --- a/webui/src/components/SearchInterface.css +++ b/webui/src/components/SearchInterface.css @@ -1,7 +1,4 @@ .search-interface { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; } .search-box { diff --git a/webui/src/components/SearchInterface.js b/webui/src/components/SearchInterface.js index 6a6ba846..3b554f8c 100644 --- a/webui/src/components/SearchInterface.js +++ b/webui/src/components/SearchInterface.js @@ -78,8 +78,8 @@ const SearchInterface = () => { }; return ( -
-

Search Documents

+
+

Search Documents

{ > Dashboard + onNavigate("/document-collections")} + isActive={isActive("/document-collections")} + > + Document Collections + { onNavigate("/create-collection")} - isActive={isActive("/create-collection")} + onClick={() => onNavigate("/search")} + isActive={isActive("/search")} > - Create New Collection + Search Documents {/* Add more menu items as needed */} diff --git a/webui/src/config/config.js b/webui/src/config/config.js index d4f57338..cd484b81 100644 --- a/webui/src/config/config.js +++ b/webui/src/config/config.js @@ -34,7 +34,8 @@ const getFullApiUrl = (route) => { const authConfig = { client_id: config.oidcClientId, - redirect_uri: getFullApiUrl(API_ROUTES.CALLBACK), + // redirect_uri: getFullApiUrl(API_ROUTES.CALLBACK), + redirect_uri: window.location.origin + '/callback', response_type: "code", scope: "openid profile email", post_logout_redirect_uri: getFullApiUrl(''), diff --git a/webui/src/contexts/AuthContext.js b/webui/src/contexts/AuthContext.js index 674bd38f..6a72fc64 100644 --- a/webui/src/contexts/AuthContext.js +++ b/webui/src/contexts/AuthContext.js @@ -1,15 +1,19 @@ -import React, { createContext, useState, useEffect, useContext } from 'react'; -import { getUserData, signIn as signInService, signOut as signOutService, handleAuthCallback, checkAuth } from '../services/authService'; -import config, { API_ROUTES } from '../config/config'; - -console.log('AuthContext initialized with config:', config); +import React, { createContext, useState, useEffect, useContext } from "react"; +import { + getUserData, + signIn as signInService, + signOut as signOutService, + handleAuthCallback, + checkAuth, +} from "../services/authService"; +import config, { API_ROUTES } from "../config/config"; const AuthContext = createContext({ user: null, loading: true, signIn: () => {}, signOut: () => {}, - fetchUser: () => {} + fetchUser: () => {}, }); export const AuthProvider = ({ children }) => { @@ -17,62 +21,51 @@ export const AuthProvider = ({ children }) => { const [loading, setLoading] = useState(true); const fetchUser = async () => { + setLoading(true); try { - console.log("Fetching user data..."); const userData = await getUserData(); - console.log("User data received:", userData); if (userData && userData.success) { setUser(userData.data); } else { - console.warn("No user data received"); setUser(null); } + setLoading(false); } catch (error) { - console.error('Error fetching user:', error); + console.error("Error fetching user:", error); setUser(null); } finally { setLoading(false); } }; - useEffect(() => { - const initAuth = async () => { - setLoading(true); - try { - console.log("Initializing authentication..."); - const authResult = handleAuthCallback(); - console.log("Auth callback result:", authResult); - if (authResult.success) { - console.log("Auth callback handled successfully, fetching user data..."); - await fetchUser(); - } else { - console.log("Checking authentication status..."); - const { authenticated } = await checkAuth(); - if (authenticated) { - console.log("User is authenticated, fetching user data..."); - await fetchUser(); - } else { - console.log("User is not authenticated"); - setUser(null); - } - } - } catch (error) { - console.error("Error during authentication initialization:", error); - setUser(null); - } finally { - setLoading(false); - } - }; + // useEffect(() => { + // const initAuth = async () => { + // // setLoading(true); + // try { + // const authResult = await handleAuthCallback(); + // if (authResult.success) { + // await fetchUser(); + // } else { + // const { authenticated } = await checkAuth(); + // if (authenticated) { + // await fetchUser(); + // } else { + // setUser(null); + // } + // } + // } catch (error) { + // console.error("Error during authentication initialization:", error); + // setUser(null); + // } finally { + // // setLoading(false); + // } + // }; - initAuth(); - }, []); + // initAuth(); + // }, []); const signIn = () => { - console.log("Initiating sign-in process..."); - console.log("Config:", config); - console.log("API_ROUTES:", API_ROUTES); if (API_ROUTES && API_ROUTES.LOGIN) { - console.log("Sign-in URL:", `${config.apiUrl}${API_ROUTES.LOGIN}`); return signInService(); } else { console.error("LOGIN route is not defined in API_ROUTES"); @@ -82,13 +75,10 @@ export const AuthProvider = ({ children }) => { const signOut = async () => { try { - console.log("Initiating sign-out process..."); - console.log("Sign-out URL:", `${config.apiUrl}${API_ROUTES.LOGOUT}`); await signOutService(); setUser(null); - console.log("Sign-out successful"); } catch (error) { - console.error('Error logging out:', error); + console.error("Error logging out:", error); } }; @@ -97,20 +87,21 @@ export const AuthProvider = ({ children }) => { loading, signIn, signOut, - fetchUser + fetchUser, }; - console.log("AuthContext value:", value); - return {children}; }; export const useAuth = () => { const context = useContext(AuthContext); if (!context) { - throw new Error('useAuth must be used within an AuthProvider'); + throw new Error("useAuth must be used within an AuthProvider"); } + + console.log("useAuth called: 1 ", context.user); + return context; }; -export default AuthContext; \ No newline at end of file +export default AuthContext; diff --git a/webui/src/services/authService.js b/webui/src/services/authService.js index cc31274c..a4e90b8a 100644 --- a/webui/src/services/authService.js +++ b/webui/src/services/authService.js @@ -1,24 +1,17 @@ import config, { API_ROUTES, getFullApiUrl } from '../config/config'; -console.log('Auth service initialized with config:', config); - export const signIn = () => { - console.log("Initiating sign-in process"); const loginUrl = getFullApiUrl(API_ROUTES.LOGIN); - console.log("Login URL:", loginUrl); return loginUrl; }; -export const handleAuthCallback = () => { - console.log("Handling auth callback"); - const urlParams = new URLSearchParams(window.location.search); +export const handleAuthCallback = async () => { + const urlParams = await new URLSearchParams(window.location.search); const token = urlParams.get('token'); - console.log("Received token:", token ? "Token present" : "No token"); if (token) { try { localStorage.setItem('jwt_token', token); - console.log("Token received and stored"); // Remove the token from the URL window.history.replaceState({}, document.title, window.location.pathname); return { success: true }; @@ -39,7 +32,6 @@ export const fetchWithAuthHeader = async (url, options = {}) => { } try { - console.log(`Fetching ${url} with auth header`); const response = await fetch(url, { ...options, headers: { @@ -55,7 +47,6 @@ export const fetchWithAuthHeader = async (url, options = {}) => { } const data = await response.json(); - console.log(`Successful response from ${url}:`, data); return data; } catch (error) { console.error("Error in fetchWithAuthHeader:", error); @@ -64,12 +55,9 @@ export const fetchWithAuthHeader = async (url, options = {}) => { }; export const getUserData = async () => { - console.log("Fetching user data..."); try { const userDataUrl = getFullApiUrl(API_ROUTES.USERINFO); - console.log("User data URL:", userDataUrl); const userData = await fetchWithAuthHeader(userDataUrl); - console.log("User data retrieved:", userData); localStorage.setItem('user_data', JSON.stringify(userData)); return { success: true, data: userData }; } catch (error) { @@ -79,12 +67,9 @@ export const getUserData = async () => { }; export const signOut = async () => { - console.log("Initiating sign-out process"); try { const logoutUrl = getFullApiUrl(API_ROUTES.LOGOUT); - console.log("Logout URL:", logoutUrl); await fetchWithAuthHeader(logoutUrl, { method: 'POST' }); - console.log("Logout successful"); return { success: true }; } catch (error) { console.error("Error during sign out:", error); @@ -95,7 +80,6 @@ export const signOut = async () => { }; const clearAuthData = () => { - console.log("Clearing auth data"); localStorage.removeItem('jwt_token'); localStorage.removeItem('user_data'); // Clear any other auth-related data here @@ -104,18 +88,14 @@ const clearAuthData = () => { export const logout = signOut; export const checkAuth = async () => { - console.log("Checking authentication status"); const token = localStorage.getItem('jwt_token'); if (!token) { - console.log("No token found, user is not authenticated"); return { isAuthenticated: false }; } try { const checkAuthUrl = getFullApiUrl(API_ROUTES.SESSION); - console.log("Check auth URL:", checkAuthUrl); const response = await fetchWithAuthHeader(checkAuthUrl); - console.log("Authentication check result:", response); return { isAuthenticated: response && response.authenticated }; } catch (error) { console.error("Error checking authentication:", error); diff --git a/webui/src/styles/global.css b/webui/src/styles/global.css index a8fed011..a2f834ff 100644 --- a/webui/src/styles/global.css +++ b/webui/src/styles/global.css @@ -5,10 +5,10 @@ body { color: #161616; } -.page-container { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; +/* Children router container common styles */ +/* Change here to apply to all children router containers */ +.children-container { + padding: 2rem 0 2rem 3rem; } .page-header { From 467f077a61c08a9fb995c8725aec237d3c08b64b Mon Sep 17 00:00:00 2001 From: Luiz Almeida Date: Fri, 11 Oct 2024 13:49:50 -0400 Subject: [PATCH 2/4] Fixed login loop. Route updated --- backend/core/config.py | 1 + backend/rag_solution/router/auth_router.py | 2 +- webui/src/App.js | 11 +- webui/src/components/Collections.js | 290 --------------------- webui/src/components/Dashboard.css | 67 ----- webui/src/components/LoginPage.js | 23 +- webui/src/contexts/AuthContext.js | 3 - webui/src/services/authService.js | 2 +- webui/src/styles/view-ui.css | 23 +- 9 files changed, 38 insertions(+), 384 deletions(-) delete mode 100644 webui/src/components/Collections.js diff --git a/backend/core/config.py b/backend/core/config.py index ae537274..56a70f4b 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -106,6 +106,7 @@ class Settings(BaseSettings): # JWT settings jwt_secret_key: str = Field(..., env='JWT_SECRET_KEY') jwt_algorithm: str = "HS256" + frontend_callback: str = "/callback" class Config: env_file = ".env" diff --git a/backend/rag_solution/router/auth_router.py b/backend/rag_solution/router/auth_router.py index 7e4ad314..73d998d3 100644 --- a/backend/rag_solution/router/auth_router.py +++ b/backend/rag_solution/router/auth_router.py @@ -156,7 +156,7 @@ async def auth(request: Request, db: Session = Depends(get_db)): custom_jwt = jwt.encode(custom_jwt_payload, settings.ibm_client_secret, algorithm="HS256") - redirect_url = f"{settings.frontend_url}/?token={custom_jwt}" + redirect_url = f"{settings.frontend_url}{settings.frontend_callback}/?token={custom_jwt}" logger.info(f"Redirecting to frontend: {redirect_url}") return RedirectResponse(url=redirect_url) diff --git a/webui/src/App.js b/webui/src/App.js index 597d2592..1f704e6a 100644 --- a/webui/src/App.js +++ b/webui/src/App.js @@ -18,10 +18,11 @@ import CollectionBrowser from "./components/CollectionBrowser"; import DocumentViewer from "./components/DocumentViewer"; import LoginPage from "./components/LoginPage"; import { handleAuthCallback } from "./services/authService"; -import Collections from "./components/Collections"; +import Collections from "./components/collection/Collections"; import "./App.css"; import "./styles/global.css"; +import CollectionForm from "./components/CollectionForm"; function AuthCallback() { const navigate = useNavigate(); @@ -51,7 +52,7 @@ const AppLayout = ({ children }) => { if (!user) { fetchUser(); } - }, [user, fetchUser]); + }, []); return (
@@ -113,7 +114,11 @@ function AppContent() { } - /> + > + }> + }> + }> + { - let navigate = useNavigate(); - /* TODO: Add a messageQueue for the application */ - const [errorMessage, setErrorMessage] = useState(""); - const [isLoadingCollections, setIsLoadingCollections] = useState(false); - const [userCollections, setUserCollections] = useState([]); - const [tableDataRows, setTableDataRows] = useState([]); - const [selectedCollection, setSelectedCollection] = useState([]); - - const headers = [ - { - key: "name", - header: "Name", - }, - { - key: "is_private", - header: "Private", - }, - { - key: "status", - header: "Status", - }, - ]; - - useEffect(() => { - fetchUserCollections(); - }, []); - - const fetchUserCollections = async () => { - setIsLoadingCollections(true); - try { - const collections = (await getUserCollections())?.collections; - console.log(collections); - // setUserCollections(Array.isArray(collections) ? collections : []); - - // datatable requires a field called id - const updatedKeys = collections.map(({ collection_id: id, ...rest }) => ({ - id, - ...rest, - })); - - // change other types fields to string - const updatedValues = updatedKeys.map((obj) => { - return obj.is_private - ? { ...obj, is_private: "true" } - : { ...obj, is_private: "false" }; - }); - setUserCollections(Array.isArray(updatedValues) ? updatedValues : []); - setTableDataRows(updatedValues.slice(0, 5)); - } catch (error) { - console.error("Error fetching user collections:", error); - setErrorMessage( - "Failed to fetch user collections. Please try again later." - ); - } finally { - // console.log(rendRows) - setIsLoadingCollections(false); - } - }; - - const createNewCollection = () => { - navigate("/create-collection"); - }; - - const onClickCollectionTable = (collectionId) => { - setSelectedCollection( - userCollections.find((collection) => collection.id === collectionId) - ); - }; - - /* TODO: fix bug with next page pagination component */ - const pageOnChange = (e) => { - setTableDataRows( - userCollections.slice((e.page - 1) * e.pageSize, e.page * e.pageSize) - ); - }; - - return ( - - <> -
-

Collections

- -
- - {isLoadingCollections ? ( -
- -
- ) : ( - <> - - {({ - rows, - headers, - getTableProps, - getHeaderProps, - getRowProps, - onInputChange, - }) => ( - <> - -
- Any header text about the data below... -
- - {/* pass in `onInputChange` change here to make filtering work */} - - - {}}> - Action 1 - - {}}> - Action 2 - - {}}> - Action 3 - - - -
- -
- - - {headers.map((header, idx) => ( - - {header.header} - - ))} - - - - - {rows.map((row, idx) => ( - onClickCollectionTable(row.id)} - > - {row.cells.map((cell) => ( - <> - {cell.value} - - ))} - - - - - - - - - ))} - -
- - )} -
- pageOnChange(e)} - pageSizes={[5, 10, 20, 50]} - size="md" - /> - - )} - - - - -
Name:
-
{selectedCollection?.name}
-
- -
ID:
-
- {selectedCollection?.collection_id} -
-
-
- - -
Private:
-
- {selectedCollection?.is_private ? "True" : "False"} -
-
- -
Status
-
{selectedCollection?.status}
-
-
- - - -
Created At:
-
- {selectedCollection?.created_at} -
-
- -
Updated At:
-
- {selectedCollection?.updated_at} -
-
-
- - -
Files:
-
- - {selectedCollection?.files?.map((file, idx) => { - return {file.filename}; - })} - -
-
-
-
- - ); -}; - -export default Collections; diff --git a/webui/src/components/Dashboard.css b/webui/src/components/Dashboard.css index a8b7b8f0..e69de29b 100644 --- a/webui/src/components/Dashboard.css +++ b/webui/src/components/Dashboard.css @@ -1,67 +0,0 @@ -.dashboard { - -} - -.dashboard-content { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - margin-top: 2rem; -} - -.dashboard-section { - background-color: #ffffff; - height: 100%; -} - -.quick-actions { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - margin-top: 1rem; -} - -.quick-actions .bx--tile { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} - -.quick-actions .bx--tile svg { - margin-bottom: 1rem; -} - -.view-all-link { - display: block; - margin-top: 1rem; - text-align: right; -} - -.usage-stats { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 1rem; - margin-top: 1rem; -} - -.stat-item { - text-align: center; -} - -.stat-item h3 { - font-size: 2rem; - margin-bottom: 0.5rem; -} - -.stat-item p { - font-size: 0.875rem; - color: #525252; -} - -@media (max-width: 768px) { - .dashboard-content { - grid-template-columns: 1fr; - } -} \ No newline at end of file diff --git a/webui/src/components/LoginPage.js b/webui/src/components/LoginPage.js index 82a0da78..b01eb945 100644 --- a/webui/src/components/LoginPage.js +++ b/webui/src/components/LoginPage.js @@ -1,28 +1,25 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Button, Loading } from 'carbon-components-react'; -import { useAuth } from '../contexts/AuthContext'; -import config, { getFullApiUrl, API_ROUTES } from '../config/config'; +import { getFullApiUrl, API_ROUTES } from '../config/config'; import './LoginPage.css'; -console.log('LoginPage component loaded with config:', config); +// console.log('LoginPage component loaded with config:', config); const LoginPage = () => { - const { signIn, user } = useAuth(); + // const { signIn, user } = useAuth(); const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - console.log('LoginPage mounted, user:', user); - console.log('Current API URL:', config.apiUrl); - console.log('Full login URL:', getFullApiUrl(API_ROUTES.LOGIN)); - }, [user]); + // useEffect(() => { + // console.log('LoginPage mounted, user:', user); + // console.log('Current API URL:', config.apiUrl); + // console.log('Full login URL:', getFullApiUrl(API_ROUTES.LOGIN)); + // }, [user]); const handleLogin = () => { - console.log('Login button clicked'); setIsLoading(true); try { - console.log('Initiating sign-in process'); const loginUrl = getFullApiUrl(API_ROUTES.LOGIN); - console.log('Redirecting to:', loginUrl); + // console.log('Redirecting to:', loginUrl); window.location.href = loginUrl; } catch (error) { console.error('Login failed:', error); diff --git a/webui/src/contexts/AuthContext.js b/webui/src/contexts/AuthContext.js index 6a72fc64..bee17ee8 100644 --- a/webui/src/contexts/AuthContext.js +++ b/webui/src/contexts/AuthContext.js @@ -98,9 +98,6 @@ export const useAuth = () => { if (!context) { throw new Error("useAuth must be used within an AuthProvider"); } - - console.log("useAuth called: 1 ", context.user); - return context; }; diff --git a/webui/src/services/authService.js b/webui/src/services/authService.js index a4e90b8a..293b674f 100644 --- a/webui/src/services/authService.js +++ b/webui/src/services/authService.js @@ -27,7 +27,7 @@ export const handleAuthCallback = async () => { export const fetchWithAuthHeader = async (url, options = {}) => { const token = localStorage.getItem('jwt_token'); if (!token) { - console.error("No token found, unable to proceed."); + console.error("No token found, unable to proceed.", url, options); return null; } diff --git a/webui/src/styles/view-ui.css b/webui/src/styles/view-ui.css index 95ed9fe5..27b2e97f 100644 --- a/webui/src/styles/view-ui.css +++ b/webui/src/styles/view-ui.css @@ -15,7 +15,6 @@ inline-size: 75px !important; } - .view-title button { display: flex; } @@ -24,22 +23,26 @@ border-top: 1px solid #ccc; margin-top: 2rem; padding-top: 1rem; - padding-inline: .5rem !important; + padding-inline: 0.5rem !important; } -.view-detail-grid > div { - margin-top: 1.5rem; +.view-detail-grid .cds--row { font-size: 14px; + display: flex; +} + +.cds--col{ + margin-top: 1.5rem; + width: 50%;; } .view-detail-label { font-weight: 600; } .view-detail-text { - padding-top: .3rem; + padding-top: 0.3rem; } - .view-title { display: flex; flex-direction: row; @@ -65,4 +68,12 @@ display: flex; /* width: 100%; */ } + + .view-detail-grid .cds--row { + flex-direction: column; + } + + .cds--col { + width: 100%; + } } From 87f144e0b04be2cc2c58cd9c90e394e3afbb71ba Mon Sep 17 00:00:00 2001 From: Luiz Almeida Date: Tue, 15 Oct 2024 00:34:30 -0400 Subject: [PATCH 3/4] authorization code, UI fixes and some code organization for react components --- ...leware.py => authentication_middleware.py} | 7 +- backend/core/authorization.py | 55 ++++ backend/core/config.py | 26 +- backend/core/loggingcors_middleware.py | 20 ++ backend/main.py | 74 ++--- backend/rag_solution/router/auth_router.py | 4 +- .../rag_solution/router/collection_router.py | 3 +- .../router/user_collection_router.py | 5 +- webui/jsconfig.json | 6 + webui/package.json | 3 +- webui/src/App.js | 37 +-- webui/src/components/Header.css | 29 -- .../src/components/collection/Collection.css | 67 ++++ webui/src/components/collection/Collection.js | 291 ++++++++++++++++++ .../CollectionForm.css} | 0 .../{ => collection}/CollectionForm.js | 5 +- .../{ => collection}/CollectionForm.test.js | 9 +- .../CollectionViewer.css} | 0 .../CollectionViewer.js} | 9 +- webui/src/components/{ => common}/Auth.js | 2 +- .../components/{ => common}/ErrorBoundary.js | 0 .../src/components/{ => common}/QueryInput.js | 0 .../components/{ => common}/ResultsDisplay.js | 0 .../{ => common}/SearchInterface.css | 0 .../{ => common}/SearchInterface.js | 4 +- .../{ => common}/SearchResultsComponent.js | 0 .../{ => common}/UserQueryComponent.js | 0 webui/src/components/dashboard/Dashboard.css | 0 .../components/{ => dashboard}/Dashboard.js | 34 +- .../{ => dashboard}/DashboardSettings.js | 0 .../{ => document}/DocumentViewer.css | 0 .../{ => document}/DocumentViewer.js | 4 +- webui/src/components/{ => layout}/Footer.css | 0 webui/src/components/{ => layout}/Footer.js | 0 webui/src/components/layout/Header.css | 70 +++++ webui/src/components/{ => layout}/Header.js | 35 ++- webui/src/components/layout/SideNav.css | 0 webui/src/components/{ => layout}/SideNav.js | 19 +- .../_NavigationBar.js} | 0 .../components/{SignIn.js => old/_SignIn.js} | 0 webui/src/pages/HomePage.js | 12 +- 41 files changed, 680 insertions(+), 150 deletions(-) rename backend/core/{auth_middleware.py => authentication_middleware.py} (92%) create mode 100644 backend/core/authorization.py create mode 100644 backend/core/loggingcors_middleware.py create mode 100644 webui/jsconfig.json delete mode 100644 webui/src/components/Header.css create mode 100644 webui/src/components/collection/Collection.css create mode 100644 webui/src/components/collection/Collection.js rename webui/src/components/{Dashboard.css => collection/CollectionForm.css} (100%) rename webui/src/components/{ => collection}/CollectionForm.js (99%) rename webui/src/components/{ => collection}/CollectionForm.test.js (95%) rename webui/src/components/{CollectionBrowser.css => collection/CollectionViewer.css} (100%) rename webui/src/components/{CollectionBrowser.js => collection/CollectionViewer.js} (98%) rename webui/src/components/{ => common}/Auth.js (97%) rename webui/src/components/{ => common}/ErrorBoundary.js (100%) rename webui/src/components/{ => common}/QueryInput.js (100%) rename webui/src/components/{ => common}/ResultsDisplay.js (100%) rename webui/src/components/{ => common}/SearchInterface.css (100%) rename webui/src/components/{ => common}/SearchInterface.js (97%) rename webui/src/components/{ => common}/SearchResultsComponent.js (100%) rename webui/src/components/{ => common}/UserQueryComponent.js (100%) create mode 100644 webui/src/components/dashboard/Dashboard.css rename webui/src/components/{ => dashboard}/Dashboard.js (75%) rename webui/src/components/{ => dashboard}/DashboardSettings.js (100%) rename webui/src/components/{ => document}/DocumentViewer.css (100%) rename webui/src/components/{ => document}/DocumentViewer.js (97%) rename webui/src/components/{ => layout}/Footer.css (100%) rename webui/src/components/{ => layout}/Footer.js (100%) create mode 100644 webui/src/components/layout/Header.css rename webui/src/components/{ => layout}/Header.js (61%) create mode 100644 webui/src/components/layout/SideNav.css rename webui/src/components/{ => layout}/SideNav.js (93%) rename webui/src/components/{NavigationBar.js => old/_NavigationBar.js} (100%) rename webui/src/components/{SignIn.js => old/_SignIn.js} (100%) diff --git a/backend/core/auth_middleware.py b/backend/core/authentication_middleware.py similarity index 92% rename from backend/core/auth_middleware.py rename to backend/core/authentication_middleware.py index 08aeea19..99c6aca3 100644 --- a/backend/core/auth_middleware.py +++ b/backend/core/authentication_middleware.py @@ -9,10 +9,10 @@ from rag_solution.file_management.database import get_db from rag_solution.services.user_service import UserService -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=settings.log_level) logger = logging.getLogger(__name__) -class AuthMiddleware(BaseHTTPMiddleware): +class AuthenticationMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): logger.info(f"AuthMiddleware: Processing request to {request.url.path}") logger.debug(f"AuthMiddleware: Request headers: {request.headers}") @@ -29,7 +29,8 @@ async def dispatch(self, request: Request, call_next): 'id': payload.get('sub'), 'email': payload.get('email'), 'name': payload.get('name'), - 'uuid': payload.get('uuid') # Extract UUID from payload + 'uuid': payload.get('uuid'), # Extract UUID from payload + 'role': payload.get('role') } logger.info(f"AuthMiddleware: JWT token validated successfully. User: {request.state.user}") except jwt.ExpiredSignatureError: diff --git a/backend/core/authorization.py b/backend/core/authorization.py new file mode 100644 index 00000000..9c12ccbe --- /dev/null +++ b/backend/core/authorization.py @@ -0,0 +1,55 @@ +import functools +import logging +import re +from fastapi import HTTPException, Request +from fastapi.responses import JSONResponse +from core.config import settings + +logging.basicConfig(level=settings.log_level) +logger = logging.getLogger(__name__) + +open_paths = ['/api/auth/login', '/api/auth/callback', '/api/health', '/api/auth/oidc-config', '/api/auth/token', '/api/auth/userinfo'] + +async def authorize_dependency(request: Request, role: str = 'admin'): + """ + Dependency to check if the user is authorized to access the resource. + + Args: + request (Request): The request object. + role (str, optional): The role required to access the resource. Defaults to 'admin'. + + Returns: + bool: True if the request is authorized, raises HTTPException otherwise. + """ + logger.info(f"AuthorizationMiddleware: Processing request to {request.url.path} by {request.state.user}") + # print(f"AuthorizationMiddleware: Processing {request.method} request to {request.url.path} by {request.state.user}") + if request.url.path in open_paths: + return True + + rrole = request.state.user.get('role') + rpath = request.url.path + try: + if rrole: + for pattern, method in settings.rbac_mapping[rrole].items(): + if re.match(pattern, rpath) and request.method in method: + return True + raise HTTPException(status_code=403, detail="Failed to authorize request. {rpath} / {rrole}") + + except (KeyError, ValueError) as exc: + logger.warning(f"Failed to authorize request. {rpath} / {rrole}") + raise HTTPException(status_code=403, detail="Failed to authorize request. {rpath} / {rrole}") from exc + +def authorize_decorator(role: str): + def decorator(handler): + @functools.wraps(handler) + async def wrapper(*args, **kwargs): + request = kwargs['request'] + # print(f"AuthorizationDecorator: Processing {request.method} request to {request.url.path} by {request.state.user}") + if request.url.path not in open_paths: + if not request.state.user or request.state.user.get('role') != role: + logger.warning(f"AuthorizationDecorator: Unauthorized request to {request.url.path}") + return JSONResponse(status_code=403, content={"detail": f"User is not authorized to access this resource (requires {role} role)"}) + return await handler(*args, **kwargs) + return wrapper + return decorator + diff --git a/backend/core/config.py b/backend/core/config.py index 56a70f4b..9ad2019a 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -38,7 +38,7 @@ class Settings(BaseSettings): react_app_api_url: str # Logging Level - log_level: Optional[str] = None + log_level: Optional[str] = "INFO" # File storage path file_storage_path: str = tempfile.gettempdir() @@ -107,11 +107,33 @@ class Settings(BaseSettings): jwt_secret_key: str = Field(..., env='JWT_SECRET_KEY') jwt_algorithm: str = "HS256" frontend_callback: str = "/callback" - + + # Role settings + # This is a sample RBAC mapping role / url_patterns / http_methods + rbac_mapping: dict = { + 'admin': { + r'^/api/user-collections/(.+)$': ['GET'], + r'^/api/user-collections/(.+)/(.+)$': ['POST', 'DELETE'], + }, + 'user': { + r'^/api/user-collections/(.+)/(.+)$': ['POST', 'DELETE'], + r'^/api/user-collections/(.+)$': ['GET'], + r'^/api/user-collections/collection/(.+)$': ['GET'], + r'^/api/user-collections/collection/(.+)/users$': ['DELETE'], + r'^/api/collections/(.+)$': ['GET'] + }, + 'guest': { + r'^/api/user-collections$': ['GET', 'POST', 'DELETE', 'PUT'], + r'^/api/collections$': ['GET', 'POST', 'DELETE', 'PUT'], + r'^/api/collection/(.+)$': ['GET', 'POST', 'DELETE', 'PUT'] + } + } + class Config: env_file = ".env" env_file_encoding = "utf-8" + settings = Settings( react_app_api_url="http://localhost:3000", ) diff --git a/backend/core/loggingcors_middleware.py b/backend/core/loggingcors_middleware.py new file mode 100644 index 00000000..9e78e270 --- /dev/null +++ b/backend/core/loggingcors_middleware.py @@ -0,0 +1,20 @@ +import logging +from fastapi import Request +from fastapi.middleware.cors import CORSMiddleware +from core.config import settings + +logging.basicConfig(level=settings.log_level) +logger = logging.getLogger(__name__) + +class LoggingCORSMiddleware(CORSMiddleware): + async def __call__(self, scope, receive, send): + if scope["type"] == "http": + logger.debug(f"CORS Request: method={scope['method']}, path={scope['path']}") + logger.debug(f"CORS Request headers: {scope['headers']}") + + response = await super().__call__(scope, receive, send) + + if scope["type"] == "http": + logger.debug(f"CORS Response headers: {response.headers}") + + return response diff --git a/backend/main.py b/backend/main.py index 99d41dcd..6744016b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,17 +4,22 @@ from fastapi import FastAPI, Depends, HTTPException, Request, Header from fastapi.openapi.utils import get_openapi -from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from starlette.middleware.sessions import SessionMiddleware from pydantic import BaseModel from sqlalchemy import inspect, text +from auth.oidc import verify_jwt_token import jwt +# Import core modules +from core.authentication_middleware import AuthenticationMiddleware +# from backend.core.authorization_decorator import AuthorizationMiddleware +from core.loggingcors_middleware import LoggingCORSMiddleware +from core.authorization import authorize_dependency from core.config import settings -from auth.oidc import get_current_user, verify_jwt_token -from rag_solution.file_management.database import Base, engine, get_db # Import all models +from rag_solution.file_management.database import Base, engine, get_db from rag_solution.models.user import User from rag_solution.models.collection import Collection from rag_solution.models.file import File @@ -22,8 +27,7 @@ from rag_solution.models.user_team import UserTeam from rag_solution.models.team import Team - -from core.auth_middleware import AuthMiddleware +# Import all routers from rag_solution.file_management.database import Base, engine from rag_solution.router.collection_router import router as collection_router from rag_solution.router.file_router import router as file_router @@ -33,24 +37,10 @@ from rag_solution.router.user_team_router import router as user_team_router from rag_solution.router.health_router import router as health_router from rag_solution.router.auth_router import router as auth_router -from auth.oidc import get_current_user, oauth logging.basicConfig(level=settings.log_level) logger = logging.getLogger(__name__) -class LoggingCORSMiddleware(CORSMiddleware): - async def __call__(self, scope, receive, send): - if scope["type"] == "http": - logger.debug(f"CORS Request: method={scope['method']}, path={scope['path']}") - logger.debug(f"CORS Request headers: {scope['headers']}") - - response = await super().__call__(scope, receive, send) - - if scope["type"] == "http": - logger.debug(f"CORS Response headers: {response.headers}") - - return response - @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Starting database initialization") @@ -123,27 +113,39 @@ async def lifespan(app: FastAPI): ) # Add Auth middleware -app.add_middleware(AuthMiddleware) - -async def auth_dependency(authorization: str = Header(...)): - try: - scheme, token = authorization.split() - if scheme.lower() != 'bearer': - raise HTTPException(status_code=401, detail="Invalid authentication scheme") - payload = verify_jwt_token(token) - return payload - except (jwt.PyJWTError, ValueError): - raise HTTPException(status_code=401, detail="Invalid or expired token") +app.add_middleware(AuthenticationMiddleware) + +# Replacing with a decorator on each endpoint to allow fine-grained authorization +# app.add_middleware(AuthorizationMiddleware) + +# Already included in AuthMiddleware +# async def auth_dependency(authorization: str = Header(...)): +# try: +# scheme, token = authorization.split() +# if scheme.lower() != 'bearer': +# raise HTTPException(status_code=401, detail="Invalid authentication scheme") +# payload = verify_jwt_token(token) +# return payload +# except (jwt.PyJWTError, ValueError): +# raise HTTPException(status_code=401, detail="Invalid or expired token") # Include routers app.include_router(auth_router) app.include_router(health_router) -app.include_router(collection_router, dependencies=[Depends(auth_dependency)]) -app.include_router(file_router, dependencies=[Depends(auth_dependency)]) -app.include_router(team_router, dependencies=[Depends(auth_dependency)]) -app.include_router(user_router, dependencies=[Depends(auth_dependency)]) -app.include_router(user_collection_router, dependencies=[Depends(auth_dependency)]) -app.include_router(user_team_router, dependencies=[Depends(auth_dependency)]) +app.include_router(collection_router) +app.include_router(file_router) +app.include_router(team_router) +app.include_router(user_router) +app.include_router(user_collection_router, dependencies=[Depends(authorize_dependency)]) +# app.include_router(user_collection_router) +app.include_router(user_team_router) +# app.include_router(collection_router, dependencies=[Depends(auth_dependency)]) +# app.include_router(file_router, dependencies=[Depends(auth_dependency)]) +# app.include_router(team_router, dependencies=[Depends(auth_dependency)]) +# app.include_router(user_router, dependencies=[Depends(auth_dependency)]) +# app.include_router(user_collection_router, dependencies=[Depends(auth_dependency)]) +# app.include_router(user_team_router, dependencies=[Depends(auth_dependency)]) +# app.include_router(collection_router, dependencies=[Depends(auth_dependency)]) def custom_openapi(): if app.openapi_schema: diff --git a/backend/rag_solution/router/auth_router.py b/backend/rag_solution/router/auth_router.py index 73d998d3..71047143 100644 --- a/backend/rag_solution/router/auth_router.py +++ b/backend/rag_solution/router/auth_router.py @@ -151,7 +151,9 @@ async def auth(request: Request, db: Session = Depends(get_db)): "sub": user['sub'], "email": user['email'], "name": user.get('name', 'Unknown'), - "uuid": str(db_user.id) # Include the UUID in the JWT payload + "uuid": str(db_user.id) , # Include the UUID in the JWT payload + "exp": token.get('expires_at'), + "role": "admin" } custom_jwt = jwt.encode(custom_jwt_payload, settings.ibm_client_secret, algorithm="HS256") diff --git a/backend/rag_solution/router/collection_router.py b/backend/rag_solution/router/collection_router.py index 2430ffb3..fc991eaf 100644 --- a/backend/rag_solution/router/collection_router.py +++ b/backend/rag_solution/router/collection_router.py @@ -9,8 +9,7 @@ from rag_solution.schemas.user_schema import UserInput from rag_solution.services.user_service import UserService from rag_solution.services.collection_service import CollectionService -from rag_solution.services.file_management_service import \ - FileManagementService +from rag_solution.services.file_management_service import FileManagementService import logging logging.basicConfig(level=logging.INFO) diff --git a/backend/rag_solution/router/user_collection_router.py b/backend/rag_solution/router/user_collection_router.py index f42db3cd..290e77ce 100644 --- a/backend/rag_solution/router/user_collection_router.py +++ b/backend/rag_solution/router/user_collection_router.py @@ -8,6 +8,8 @@ from rag_solution.services.user_collection_interaction_service import UserCollectionInteractionService from rag_solution.schemas.user_collection_schema import UserCollectionOutput, UserCollectionsOutput +from core.authorization import authorize_decorator + router = APIRouter(prefix="/api/user-collections", tags=["user-collections"]) @router.post("/{user_id}/{collection_id}", @@ -48,7 +50,8 @@ def remove_user_from_collection(user_id: UUID, collection_id: UUID, db: Session 500: {"description": "Internal server error"} } ) -def get_user_collections(user_id: UUID, request: Request, db: Session = Depends(get_db)): +@authorize_decorator(role="admin") +async def get_user_collections(user_id: UUID, request: Request, db: Session = Depends(get_db)): if not hasattr(request.state, 'user') or request.state.user['uuid'] != str(user_id): raise HTTPException(status_code=403, detail="Not authorized to access this resource") diff --git a/webui/jsconfig.json b/webui/jsconfig.json new file mode 100644 index 00000000..b8fc3230 --- /dev/null +++ b/webui/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src"] +} \ No newline at end of file diff --git a/webui/package.json b/webui/package.json index 8d0918b4..59bac685 100644 --- a/webui/package.json +++ b/webui/package.json @@ -23,7 +23,8 @@ "start": "react-scripts start", "build": "DISABLE_ESLINT_PLUGIN=true react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "dev": "WDS_SOCKET_PORT=3000 CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true FAST_REFRESH=false react-scripts start" }, "proxy": "http://localhost:8000", "eslintConfig": { diff --git a/webui/src/App.js b/webui/src/App.js index 1f704e6a..856b4b1d 100644 --- a/webui/src/App.js +++ b/webui/src/App.js @@ -10,19 +10,20 @@ import { import { Content } from "carbon-components-react"; import { AuthProvider, useAuth } from "./contexts/AuthContext"; import { NotificationProvider } from "./contexts/NotificationContext"; -import Header from "./components/Header"; -import Footer from "./components/Footer"; -import Dashboard from "./components/Dashboard"; -import SearchInterface from "./components/SearchInterface"; -import CollectionBrowser from "./components/CollectionBrowser"; -import DocumentViewer from "./components/DocumentViewer"; +import Header from "./components/layout/Header"; +import Footer from "./components/layout/Footer"; +import Dashboard from "./components/dashboard/Dashboard"; +import SearchInterface from "./components/common/SearchInterface"; +import DocumentViewer from "./components/document/DocumentViewer"; import LoginPage from "./components/LoginPage"; import { handleAuthCallback } from "./services/authService"; -import Collections from "./components/collection/Collections"; + +import Collection from "./components/collection/Collection"; +import CollectionForm from "./components/collection/CollectionForm"; +import CollectionViewer from "./components/collection/CollectionViewer"; import "./App.css"; import "./styles/global.css"; -import CollectionForm from "./components/CollectionForm"; function AuthCallback() { const navigate = useNavigate(); @@ -99,23 +100,15 @@ function AppContent() { } /> - - - - } - /> - + } > - }> + }> }> }> @@ -127,6 +120,14 @@ function AppContent() { } /> + + + + } + /> } /> ); diff --git a/webui/src/components/Header.css b/webui/src/components/Header.css deleted file mode 100644 index e5f866b8..00000000 --- a/webui/src/components/Header.css +++ /dev/null @@ -1,29 +0,0 @@ -.bx--header__nav::before { - display: none; -} - -.bx--header__menu-item { - position: relative; -} - -.bx--header__menu-item.active::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 3px; - background-color: #0f62fe; -} - -.bx--header__menu-item:hover { - background-color: #353535; -} - -.bx--header__action:hover { - background-color: #353535; -} - -.bx--header__name:hover { - background-color: #353535; -} \ No newline at end of file diff --git a/webui/src/components/collection/Collection.css b/webui/src/components/collection/Collection.css new file mode 100644 index 00000000..a8b7b8f0 --- /dev/null +++ b/webui/src/components/collection/Collection.css @@ -0,0 +1,67 @@ +.dashboard { + +} + +.dashboard-content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +.dashboard-section { + background-color: #ffffff; + height: 100%; +} + +.quick-actions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.quick-actions .bx--tile { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.quick-actions .bx--tile svg { + margin-bottom: 1rem; +} + +.view-all-link { + display: block; + margin-top: 1rem; + text-align: right; +} + +.usage-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.stat-item { + text-align: center; +} + +.stat-item h3 { + font-size: 2rem; + margin-bottom: 0.5rem; +} + +.stat-item p { + font-size: 0.875rem; + color: #525252; +} + +@media (max-width: 768px) { + .dashboard-content { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/webui/src/components/collection/Collection.js b/webui/src/components/collection/Collection.js new file mode 100644 index 00000000..ee538386 --- /dev/null +++ b/webui/src/components/collection/Collection.js @@ -0,0 +1,291 @@ +import React, { useEffect, useState } from "react"; +import { Outlet, useNavigate } from "react-router-dom"; +import { + Button, + Content, + Row, + DataTable, + Loading, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + TableToolbar, + TableToolbarAction, + TableToolbarContent, + TableToolbarMenu, + TableToolbarSearch, + OverflowMenu, + OverflowMenuItem, + Column, + FlexGrid, + UnorderedList, + ListItem, + Pagination, +} from "@carbon/react"; +import { Add } from "@carbon/icons-react"; + +import { getUserCollections } from "src/api/api"; +import "./Collection.css"; +import "src/styles/view-ui.css"; + +const Collections = () => { + let navigate = useNavigate(); + /* TODO: Add a messageQueue for the application */ + const [errorMessage, setErrorMessage] = useState(""); + const [isLoadingCollections, setIsLoadingCollections] = useState(false); + const [userCollections, setUserCollections] = useState([]); + const [tableDataRows, setTableDataRows] = useState([]); + const [selectedCollection, setSelectedCollection] = useState([]); + + const headers = [ + { + key: "name", + header: "Name", + }, + { + key: "is_private", + header: "Private", + }, + { + key: "status", + header: "Status", + }, + ]; + + useEffect(() => { + fetchUserCollections(); + }, []); + + const fetchUserCollections = async () => { + setIsLoadingCollections(true); + try { + const collections = (await getUserCollections())?.collections; + console.log(collections); + // setUserCollections(Array.isArray(collections) ? collections : []); + + // datatable requires a field called id + const updatedKeys = collections.map(({ collection_id: id, ...rest }) => ({ + id, + ...rest, + })); + + // change other types fields to string + const updatedValues = updatedKeys.map((obj) => { + return obj.is_private + ? { ...obj, is_private: "true" } + : { ...obj, is_private: "false" }; + }); + setUserCollections(Array.isArray(updatedValues) ? updatedValues : []); + setTableDataRows(updatedValues.slice(0, 5)); + } catch (error) { + console.error("Error fetching user collections:", error); + setErrorMessage( + "Failed to fetch user collections. Please try again later." + ); + } finally { + // console.log(rendRows) + setIsLoadingCollections(false); + } + }; + + const createNewCollection = () => { + navigate("/collections/create", { replace: true }); + }; + + const onClickCollectionTable = (collectionId) => { + setSelectedCollection( + userCollections.find((collection) => collection.id === collectionId) + ); + }; + + /* TODO: fix bug with next page pagination component */ + const pageOnChange = (e) => { + setTableDataRows( + userCollections.slice((e.page - 1) * e.pageSize, e.page * e.pageSize) + ); + }; + + return ( + + <> +
+

Collections

+ +
+ + {isLoadingCollections ? ( +
+ +
+ ) : ( + <> + + {({ + rows, + headers, + getTableProps, + getHeaderProps, + getRowProps, + onInputChange, + }) => ( + <> + +
+ Any header text about the data below... +
+ + {/* pass in `onInputChange` change here to make filtering work */} + + + {}}> + Action 1 + + {}}> + Action 2 + + {}}> + Action 3 + + + +
+ + + + + {headers.map((header, idx) => ( + + {header.header} + + ))} + + + + + {rows.map((row, idx) => ( + onClickCollectionTable(row.id)} + > + {row.cells.map((cell) => ( + <> + {cell.value} + + ))} + + + + + + + + + ))} + +
+ + )} +
+ pageOnChange(e)} + pageSizes={[5, 10, 20, 50]} + size="md" + /> + + )} + + + + +
Name:
+
{selectedCollection?.name}
+
+ +
ID:
+
+ {selectedCollection?.collection_id} +
+
+
+ + +
Private:
+
+ {selectedCollection?.is_private ? "True" : "False"} +
+
+ +
Status
+
{selectedCollection?.status}
+
+
+ + + +
Created At:
+
+ {selectedCollection?.created_at} +
+
+ +
Updated At:
+
+ {selectedCollection?.updated_at} +
+
+
+ + +
Files:
+
+ + {selectedCollection?.files?.map((file, idx) => { + return {file.filename}; + })} + +
+
+
+
+ +
+ ); +}; + +export default Collections; diff --git a/webui/src/components/Dashboard.css b/webui/src/components/collection/CollectionForm.css similarity index 100% rename from webui/src/components/Dashboard.css rename to webui/src/components/collection/CollectionForm.css diff --git a/webui/src/components/CollectionForm.js b/webui/src/components/collection/CollectionForm.js similarity index 99% rename from webui/src/components/CollectionForm.js rename to webui/src/components/collection/CollectionForm.js index ce76ff22..50c5decb 100644 --- a/webui/src/components/CollectionForm.js +++ b/webui/src/components/collection/CollectionForm.js @@ -16,8 +16,9 @@ import { Column } from '@carbon/react'; import { TrashCan, Document } from '@carbon/icons-react'; -import { createCollectionWithDocuments, getUserCollections } from '../api/api'; -import { useAuth } from '../contexts/AuthContext'; + +import { createCollectionWithDocuments, getUserCollections } from 'src/api/api'; +import { useAuth } from 'src/contexts/AuthContext'; const CollectionForm = () => { const { user, loading: authLoading } = useAuth(); diff --git a/webui/src/components/CollectionForm.test.js b/webui/src/components/collection/CollectionForm.test.js similarity index 95% rename from webui/src/components/CollectionForm.test.js rename to webui/src/components/collection/CollectionForm.test.js index 335254b8..d2a65907 100644 --- a/webui/src/components/CollectionForm.test.js +++ b/webui/src/components/collection/CollectionForm.test.js @@ -1,9 +1,9 @@ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import CollectionForm from './CollectionForm'; -import { AuthProvider } from '../contexts/AuthContext'; -import { createCollectionWithDocuments, getUserCollections } from '../api/api'; +import CollectionForm from './collection/CollectionForm'; +import { AuthProvider } from 'src/contexts/AuthContext'; +import { createCollectionWithDocuments, getUserCollections } from 'src/api/api'; // Mock the API functions jest.mock('../api/api', () => ({ @@ -93,6 +93,9 @@ describe('CollectionForm', () => { await waitFor(() => { expect(createCollectionWithDocuments).toHaveBeenCalled(); + }); + + await waitFor(() => { expect(screen.getByText('Collection Created')).toBeInTheDocument(); }); }); diff --git a/webui/src/components/CollectionBrowser.css b/webui/src/components/collection/CollectionViewer.css similarity index 100% rename from webui/src/components/CollectionBrowser.css rename to webui/src/components/collection/CollectionViewer.css diff --git a/webui/src/components/CollectionBrowser.js b/webui/src/components/collection/CollectionViewer.js similarity index 98% rename from webui/src/components/CollectionBrowser.js rename to webui/src/components/collection/CollectionViewer.js index 8f225607..9b987ef1 100644 --- a/webui/src/components/CollectionBrowser.js +++ b/webui/src/components/collection/CollectionViewer.js @@ -20,11 +20,12 @@ import { ModalHeader, ModalBody, ModalFooter -} from 'carbon-components-react'; +} from "@carbon/react"; import { Add, TrashCan, Document, Edit} from '@carbon/icons-react'; -import { getUserCollections, createCollectionWithDocuments, updateCollection, deleteCollection, getDocumentsInCollection, deleteDocument, moveDocument } from '../api/api'; -import { useNotification } from '../contexts/NotificationContext'; -import './CollectionBrowser.css'; +import { getUserCollections, createCollectionWithDocuments, updateCollection, deleteCollection, getDocumentsInCollection, deleteDocument, moveDocument } from '../../api/api'; +import { useNotification } from 'src/contexts/NotificationContext'; + +import './CollectionViewer.css'; const CollectionBrowser = () => { const [collections, setCollections] = useState([]); diff --git a/webui/src/components/Auth.js b/webui/src/components/common/Auth.js similarity index 97% rename from webui/src/components/Auth.js rename to webui/src/components/common/Auth.js index d5a8b183..a7aa2b4b 100644 --- a/webui/src/components/Auth.js +++ b/webui/src/components/common/Auth.js @@ -1,5 +1,5 @@ import React, { useEffect, useState, useCallback, memo } from 'react'; -import { signIn, signOut, getUserData, loadIBMScripts, handleAuthCallback } from '../services/authService'; +import { signIn, signOut, getUserData, loadIBMScripts, handleAuthCallback } from 'src/services/authService'; const ErrorMessage = memo(({ message }) =>
Error: {message}
); const SignInButton = memo(({ onSignIn }) => ); diff --git a/webui/src/components/ErrorBoundary.js b/webui/src/components/common/ErrorBoundary.js similarity index 100% rename from webui/src/components/ErrorBoundary.js rename to webui/src/components/common/ErrorBoundary.js diff --git a/webui/src/components/QueryInput.js b/webui/src/components/common/QueryInput.js similarity index 100% rename from webui/src/components/QueryInput.js rename to webui/src/components/common/QueryInput.js diff --git a/webui/src/components/ResultsDisplay.js b/webui/src/components/common/ResultsDisplay.js similarity index 100% rename from webui/src/components/ResultsDisplay.js rename to webui/src/components/common/ResultsDisplay.js diff --git a/webui/src/components/SearchInterface.css b/webui/src/components/common/SearchInterface.css similarity index 100% rename from webui/src/components/SearchInterface.css rename to webui/src/components/common/SearchInterface.css diff --git a/webui/src/components/SearchInterface.js b/webui/src/components/common/SearchInterface.js similarity index 97% rename from webui/src/components/SearchInterface.js rename to webui/src/components/common/SearchInterface.js index 3b554f8c..566eee6e 100644 --- a/webui/src/components/SearchInterface.js +++ b/webui/src/components/common/SearchInterface.js @@ -13,8 +13,8 @@ import { InlineLoading } from 'carbon-components-react'; import { Search, Filter } from '@carbon/icons-react'; -import { getUserCollections } from '../api/api'; // Removed queryCollection import -import { useNotification } from '../contexts/NotificationContext'; +import { getUserCollections } from '../../api/api'; // Removed queryCollection import +import { useNotification } from '../../contexts/NotificationContext'; import './SearchInterface.css'; const SearchInterface = () => { diff --git a/webui/src/components/SearchResultsComponent.js b/webui/src/components/common/SearchResultsComponent.js similarity index 100% rename from webui/src/components/SearchResultsComponent.js rename to webui/src/components/common/SearchResultsComponent.js diff --git a/webui/src/components/UserQueryComponent.js b/webui/src/components/common/UserQueryComponent.js similarity index 100% rename from webui/src/components/UserQueryComponent.js rename to webui/src/components/common/UserQueryComponent.js diff --git a/webui/src/components/dashboard/Dashboard.css b/webui/src/components/dashboard/Dashboard.css new file mode 100644 index 00000000..e69de29b diff --git a/webui/src/components/Dashboard.js b/webui/src/components/dashboard/Dashboard.js similarity index 75% rename from webui/src/components/Dashboard.js rename to webui/src/components/dashboard/Dashboard.js index 4027999b..320f2733 100644 --- a/webui/src/components/Dashboard.js +++ b/webui/src/components/dashboard/Dashboard.js @@ -12,8 +12,8 @@ import { Button, } from "carbon-components-react"; import { Add, Search, Document } from "@carbon/icons-react"; -import { getUserCollections } from "../api/api"; -import { useNotification } from "../contexts/NotificationContext"; +import { getUserCollections } from "src/api/api"; +import { useNotification } from "src/contexts/NotificationContext"; import "./Dashboard.css"; const Dashboard = () => { @@ -25,6 +25,7 @@ const Dashboard = () => { fetchDashboardData(); }, []); + const fetchDashboardData = async () => { setLoading(true); try { @@ -61,7 +62,7 @@ const Dashboard = () => {

Welcome to IBM RAG Solution

- +

Quick Actions

@@ -81,28 +82,28 @@ const Dashboard = () => {
- -

Recent Collections

- - - - Name - Documents - Last Updated + +

Recent Collections

+ + + + Name + Documents + Last Updated - + {collections?.map((collection) => ( - - + + {collection.name} - + {collection.documentCount} - + {new Date(collection.lastUpdated).toLocaleDateString()} @@ -118,4 +119,5 @@ const Dashboard = () => { ); }; + export default Dashboard; diff --git a/webui/src/components/DashboardSettings.js b/webui/src/components/dashboard/DashboardSettings.js similarity index 100% rename from webui/src/components/DashboardSettings.js rename to webui/src/components/dashboard/DashboardSettings.js diff --git a/webui/src/components/DocumentViewer.css b/webui/src/components/document/DocumentViewer.css similarity index 100% rename from webui/src/components/DocumentViewer.css rename to webui/src/components/document/DocumentViewer.css diff --git a/webui/src/components/DocumentViewer.js b/webui/src/components/document/DocumentViewer.js similarity index 97% rename from webui/src/components/DocumentViewer.js rename to webui/src/components/document/DocumentViewer.js index bbf09d69..431f34f7 100644 --- a/webui/src/components/DocumentViewer.js +++ b/webui/src/components/document/DocumentViewer.js @@ -19,8 +19,8 @@ import { } from 'carbon-components-react'; import { Document, Page } from 'react-pdf'; import { Download, Edit } from '@carbon/icons-react'; -import { getDocument } from '../api/api'; // Removed updateDocumentMetadata import -import { useNotification } from '../contexts/NotificationContext'; +import { getDocument } from 'src/api/api'; // Removed updateDocumentMetadata import +import { useNotification } from 'src/contexts/NotificationContext'; import './DocumentViewer.css'; const DocumentViewer = () => { diff --git a/webui/src/components/Footer.css b/webui/src/components/layout/Footer.css similarity index 100% rename from webui/src/components/Footer.css rename to webui/src/components/layout/Footer.css diff --git a/webui/src/components/Footer.js b/webui/src/components/layout/Footer.js similarity index 100% rename from webui/src/components/Footer.js rename to webui/src/components/layout/Footer.js diff --git a/webui/src/components/layout/Header.css b/webui/src/components/layout/Header.css new file mode 100644 index 00000000..046638be --- /dev/null +++ b/webui/src/components/layout/Header.css @@ -0,0 +1,70 @@ +/* .bx--header__nav::before { + display: none; +} + +.bx--header__menu-item { + position: relative; +} + +.bx--header__menu-item.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3px; + background-color: #0f62fe; +} + +.bx--header__menu-item:hover { + background-color: #353535; +} + +.bx--header__action:hover { + background-color: #353535; +} + +.bx--header__name:hover { + background-color: #353535; +} */ + +.user-menu { + padding: 0; +} + +.user-menu-panel { + height: fit-content; +} + +.user-menu-username { + font-size: 1.2rem !important; + margin-bottom: 10px; + margin-top: 5px; +} +.user-menu-list { + margin: 0 !important; +} + +.user-menu-list li { + font-size: 0.9rem; + padding: 5px 5px 5px 15px; + cursor: pointer; +} + +.user-menu-list li::before { + display: none; +} + +.user-menu-logout { + width: 100%; + background-color: #353535; + margin-top: 5px; +} + +.user-menu-logout > a { + display: block; + width: 100%; + text-decoration: none; + padding-top: 5px; + padding-bottom: 5px; +} \ No newline at end of file diff --git a/webui/src/components/Header.js b/webui/src/components/layout/Header.js similarity index 61% rename from webui/src/components/Header.js rename to webui/src/components/layout/Header.js index 188e5a45..f4dcc4f4 100644 --- a/webui/src/components/Header.js +++ b/webui/src/components/layout/Header.js @@ -6,11 +6,14 @@ import { HeaderGlobalAction, HeaderPanel, Theme, + UnorderedList, + ListItem, } from "@carbon/react"; -import { Menu, UserAvatar } from "@carbon/icons-react"; -import { useAuth } from "../contexts/AuthContext"; +import { Menu, UserAvatar, CloseLarge, List } from "@carbon/icons-react"; +import { useAuth } from "src/contexts/AuthContext"; import UISideNav from "./SideNav"; +import "./Header.css"; const Header = () => { const { user, signOut } = useAuth(); @@ -36,7 +39,7 @@ const Header = () => { - + {isMainMenuOpen ? : } RAG Modulo @@ -47,7 +50,7 @@ const Header = () => { onClick={handleUserMenuClick} isActive={isUserMenuOpen} > - + {isUserMenuOpen && ( @@ -57,14 +60,22 @@ const Header = () => { className="user-menu-panel" >
- {user ? ( - <> -

{user.name || "User"}

- - - ) : ( - Sign In - )} + + {user ? ( + <> + {user.name || "User"} + Profile + Settings + + Logout + + + ) : ( + + Sign In + + )} +
)} diff --git a/webui/src/components/layout/SideNav.css b/webui/src/components/layout/SideNav.css new file mode 100644 index 00000000..e69de29b diff --git a/webui/src/components/SideNav.js b/webui/src/components/layout/SideNav.js similarity index 93% rename from webui/src/components/SideNav.js rename to webui/src/components/layout/SideNav.js index bb8a0394..a9a3e851 100644 --- a/webui/src/components/SideNav.js +++ b/webui/src/components/layout/SideNav.js @@ -1,5 +1,5 @@ import React from "react"; -import { SideNav, SideNavItems, SideNavLink } from "@carbon/react"; +import { SideNav, SideNavDivider, SideNavItems, SideNavLink } from "@carbon/react"; import { useLocation, useNavigate } from "react-router-dom"; import { Fade} from '@carbon/icons-react'; @@ -31,14 +31,6 @@ const UISideNav = ({ isSideNavExpanded, handleSideNavExpand }) => { > Dashboard - onNavigate("/document-collections")} - isActive={isActive("/document-collections")} - > - Document Collections - { > Search Documents + + onNavigate("/document-collections")} + isActive={isActive("/document-collections")} + > + Document Collections + {/* Add more menu items as needed */} diff --git a/webui/src/components/NavigationBar.js b/webui/src/components/old/_NavigationBar.js similarity index 100% rename from webui/src/components/NavigationBar.js rename to webui/src/components/old/_NavigationBar.js diff --git a/webui/src/components/SignIn.js b/webui/src/components/old/_SignIn.js similarity index 100% rename from webui/src/components/SignIn.js rename to webui/src/components/old/_SignIn.js diff --git a/webui/src/pages/HomePage.js b/webui/src/pages/HomePage.js index 878d7bac..17f2a1fd 100644 --- a/webui/src/pages/HomePage.js +++ b/webui/src/pages/HomePage.js @@ -5,13 +5,13 @@ import { ClickableTile, } from '@carbon/react'; import { Settings as SettingsIcon } from '@carbon/icons-react'; -import Header from '../components/Header.js'; -import UISideNav from '../components/SideNav.js'; -import QueryInput from '../components/QueryInput'; -import ResultsDisplay from '../components/ResultsDisplay'; -import DashboardSettings from '../components/DashboardSettings'; +import Header from '../components/layout/Header.js'; +import UISideNav from '../components/layout/SideNav.js'; +import QueryInput from '../components/common/QueryInput.js'; +import ResultsDisplay from '../components/common/ResultsDisplay.js'; +import DashboardSettings from '../components/dashboard/DashboardSettings.js'; import IngestionSettings from '../components/IngestionSettings'; -import CollectionForm from '../components/CollectionForm'; +import CollectionForm from '../components/collection/CollectionForm.js'; import '../css/common.css'; const HomePage = () => { From ef69068df2a219f4d26f6105bd4ae77946277488 Mon Sep 17 00:00:00 2001 From: Luiz Almeida Date: Tue, 15 Oct 2024 00:44:38 -0400 Subject: [PATCH 4/4] update docstring for decorator and dependency function --- backend/core/authorization.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/core/authorization.py b/backend/core/authorization.py index 9c12ccbe..fc652c26 100644 --- a/backend/core/authorization.py +++ b/backend/core/authorization.py @@ -10,13 +10,13 @@ open_paths = ['/api/auth/login', '/api/auth/callback', '/api/health', '/api/auth/oidc-config', '/api/auth/token', '/api/auth/userinfo'] -async def authorize_dependency(request: Request, role: str = 'admin'): +async def authorize_dependency(request: Request): """ Dependency to check if the user is authorized to access the resource. + Uses the RBAC mapping from settings.rbac_mapping to check if the user is authorized to access the resource. Args: request (Request): The request object. - role (str, optional): The role required to access the resource. Defaults to 'admin'. Returns: bool: True if the request is authorized, raises HTTPException otherwise. @@ -40,6 +40,15 @@ async def authorize_dependency(request: Request, role: str = 'admin'): raise HTTPException(status_code=403, detail="Failed to authorize request. {rpath} / {rrole}") from exc def authorize_decorator(role: str): + """ + Decorator to check if the user is authorized to access the resource. + + Args: + role (str): The role required to access the resource. + + Returns: + function: Goes to the original handler (function) if the request is authorized, raises HTTPException otherwise. + """ def decorator(handler): @functools.wraps(handler) async def wrapper(*args, **kwargs):