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..fc652c26 --- /dev/null +++ b/backend/core/authorization.py @@ -0,0 +1,64 @@ +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): + """ + 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. + + 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): + """ + 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): + 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 ae537274..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() @@ -106,11 +106,34 @@ class Settings(BaseSettings): # JWT settings 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 7e4ad314..71047143 100644 --- a/backend/rag_solution/router/auth_router.py +++ b/backend/rag_solution/router/auth_router.py @@ -151,12 +151,14 @@ 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") - 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/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.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..856b4b1d 100644 --- a/webui/src/App.js +++ b/webui/src/App.js @@ -1,75 +1,42 @@ -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/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 Collection from "./components/collection/Collection"; +import CollectionForm from "./components/collection/CollectionForm"; +import CollectionViewer from "./components/collection/CollectionViewer"; + +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 +46,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]); + }, []); + + 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 ( } /> @@ -120,10 +104,14 @@ function AppContent() { path="/collections" element={ - + } - /> + > + }> + }> + }> + } /> + + + + } + /> } /> ); } -function App() { - console.log('App component rendered', { windowLocation: window.location }); - +const App = () => { return ( @@ -151,6 +145,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/Dashboard.js b/webui/src/components/Dashboard.js deleted file mode 100644 index 030f9e04..00000000 --- a/webui/src/components/Dashboard.js +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; -import { - Tile, - ClickableTile, - Loading, - StructuredListWrapper, - StructuredListHead, - 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'; - -const Dashboard = () => { - const [collections, setCollections] = useState([]); - const [loading, setLoading] = useState(true); - const { addNotification } = useNotification(); - - useEffect(() => { - fetchDashboardData(); - }, []); - - const fetchDashboardData = async () => { - setLoading(true); - try { - const collectionsData = await getUserCollections(1, 5); - console.log('Fetched collections data:', collectionsData); - setCollections(collectionsData.data || []); - } catch (error) { - console.error('Error fetching dashboard data:', error); - addNotification('error', 'Error', 'Failed to load dashboard data. Please try again later.'); - setCollections([]); - } finally { - setLoading(false); - } - }; - - if (loading) { - return ; - } - - if (collections.length === 0) { - return ( -
-

Welcome to IBM RAG Solution

-

No collections found. Start by creating a new collection.

-
- ); - } - - return ( -
-

Welcome to IBM RAG Solution

-
- -

Quick Actions

-
- - -

Search Documents

-

Search across all your collections

-
- - -

Create Collection

-

Add a new document collection

-
- - -

Upload Document

-

Add a new document to a collection

-
-
-
- -

Recent Collections

- - - - Name - Documents - Last Updated - - - - {collections.map((collection) => ( - - - {collection.name} - - {collection.documentCount} - {new Date(collection.lastUpdated).toLocaleDateString()} - - ))} - - - - - -
-
-
- ); -}; - -export default Dashboard; 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/Header.js b/webui/src/components/Header.js deleted file mode 100644 index 930c8007..00000000 --- a/webui/src/components/Header.js +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { - Header as CarbonHeader, - HeaderName, - HeaderNavigation, - HeaderMenuItem, - HeaderGlobalBar, - HeaderGlobalAction, - SkipToContent, -} from 'carbon-components-react'; -import { User, Logout } from '@carbon/icons-react'; -import { useAuth } from '../contexts/AuthContext'; -import './Header.css'; - -const Header = () => { - const { isAuthenticated, user, logout } = useAuth(); - const location = useLocation(); - - const isActive = (path) => { - return location.pathname === path ? 'active' : ''; - }; - - return ( - - - - RAG Solution - - - - Dashboard - - - Search - - - Collections - - - - {isAuthenticated && ( - <> - {}}> - - - - - - - )} - - - ); -}; - -export default Header; 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/components/Dashboard.css b/webui/src/components/collection/Collection.css similarity index 94% rename from webui/src/components/Dashboard.css rename to webui/src/components/collection/Collection.css index ada5bb6c..a8b7b8f0 100644 --- a/webui/src/components/Dashboard.css +++ b/webui/src/components/collection/Collection.css @@ -1,7 +1,5 @@ .dashboard { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; + } .dashboard-content { diff --git a/webui/src/components/Collections.js b/webui/src/components/collection/Collection.js similarity index 96% rename from webui/src/components/Collections.js rename to webui/src/components/collection/Collection.js index a037675a..ee538386 100644 --- a/webui/src/components/Collections.js +++ b/webui/src/components/collection/Collection.js @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - +import { Outlet, useNavigate } from "react-router-dom"; import { Button, Content, @@ -24,12 +23,13 @@ import { FlexGrid, UnorderedList, ListItem, - Pagination + Pagination, } from "@carbon/react"; import { Add } from "@carbon/icons-react"; -import { getUserCollections } from "../api/api"; -import "../styles/view-ui.css"; +import { getUserCollections } from "src/api/api"; +import "./Collection.css"; +import "src/styles/view-ui.css"; const Collections = () => { let navigate = useNavigate(); @@ -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 @@ -91,7 +92,7 @@ const Collections = () => { }; const createNewCollection = () => { - navigate("/create-collection"); + navigate("/collections/create", { replace: true }); }; const onClickCollectionTable = (collectionId) => { @@ -108,7 +109,7 @@ const Collections = () => { }; return ( - + <>

Collections

@@ -282,6 +283,7 @@ const Collections = () => { + ); }; diff --git a/webui/src/components/collection/CollectionForm.css b/webui/src/components/collection/CollectionForm.css new file mode 100644 index 00000000..e69de29b diff --git a/webui/src/components/CollectionForm.js b/webui/src/components/collection/CollectionForm.js similarity index 98% rename from webui/src/components/CollectionForm.js rename to webui/src/components/collection/CollectionForm.js index 0faaadae..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(); @@ -144,7 +145,7 @@ const CollectionForm = () => { } return ( -
+

Your Collections

{isLoadingCollections ? ( 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 89% rename from webui/src/components/CollectionBrowser.css rename to webui/src/components/collection/CollectionViewer.css index 32b2853e..aeda608a 100644 --- a/webui/src/components/CollectionBrowser.css +++ b/webui/src/components/collection/CollectionViewer.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/collection/CollectionViewer.js similarity index 94% rename from webui/src/components/CollectionBrowser.js rename to webui/src/components/collection/CollectionViewer.js index 7208eb04..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([]); @@ -199,7 +200,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 +236,7 @@ const CollectionBrowser = () => { ), })); - const documentRows = documents.map((document) => ({ + const documentRows = documents?.map((document) => ({ id: document.id, name: document.name, type: document.type, @@ -262,8 +263,9 @@ const CollectionBrowser = () => { })); return ( -
-

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

+ +
+

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

{ ) : ( <> - + {({ rows, headers, getHeaderProps, getTableProps }) => ( @@ -389,8 +391,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/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 95% rename from webui/src/components/SearchInterface.css rename to webui/src/components/common/SearchInterface.css index c41ada9f..c2470f5c 100644 --- a/webui/src/components/SearchInterface.css +++ b/webui/src/components/common/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/common/SearchInterface.js similarity index 95% rename from webui/src/components/SearchInterface.js rename to webui/src/components/common/SearchInterface.js index 6a6ba846..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 = () => { @@ -78,8 +78,8 @@ const SearchInterface = () => { }; return ( -
-

Search Documents

+
+

Search Documents

{ + const [collections, setCollections] = useState([]); + const [loading, setLoading] = useState(true); + const { addNotification } = useNotification(); + + useEffect(() => { + fetchDashboardData(); + }, []); + + + const fetchDashboardData = async () => { + setLoading(true); + try { + 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." + ); + setCollections([]); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ; + } + + if (collections?.length === 0) { + return ( +
+

Welcome to IBM RAG Solution

+

No collections found. Start by creating a new collection.

+
+ ); + } + + return ( +
+

Welcome to IBM RAG Solution

+
+ +

Quick Actions

+
+ + +

Search Documents

+

Search across all your collections

+
+ + +

Create Collection

+

Add a new document collection

+
+ + +

Upload Document

+

Add a new document to a collection

+
+
+
+ +

Recent Collections

+ + + + Name + Documents + Last Updated + + + + {collections?.map((collection) => ( + + + + {collection.name} + + + + {collection.documentCount} + + + {new Date(collection.lastUpdated).toLocaleDateString()} + + + ))} + + + + + +
+
+
+ ); +}; + + +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 95% rename from webui/src/components/DocumentViewer.css rename to webui/src/components/document/DocumentViewer.css index f560bb61..1cc15af2 100644 --- a/webui/src/components/DocumentViewer.css +++ b/webui/src/components/document/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/document/DocumentViewer.js similarity index 96% rename from webui/src/components/DocumentViewer.js rename to webui/src/components/document/DocumentViewer.js index 8c3b3970..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 = () => { @@ -129,7 +129,7 @@ const DocumentViewer = () => { } return ( -
+
Home Collections @@ -140,7 +140,8 @@ const DocumentViewer = () => {
-

{document.title}

+

{document.title}

+