diff --git a/backend/modules/admin/__pycache__/api.cpython-312.pyc b/backend/modules/admin/__pycache__/api.cpython-312.pyc index 275f34e..6f2b3a9 100644 Binary files a/backend/modules/admin/__pycache__/api.cpython-312.pyc and b/backend/modules/admin/__pycache__/api.cpython-312.pyc differ diff --git a/backend/modules/admin/api.py b/backend/modules/admin/api.py index 048b1d9..9ca6f98 100644 --- a/backend/modules/admin/api.py +++ b/backend/modules/admin/api.py @@ -1,6 +1,7 @@ # modules/admin/api.py from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Body # Import Body +from pydantic import BaseModel # Import BaseModel from sqlalchemy.orm import Session from core.database import Base, get_db from modules.auth.models import User, UserRole @@ -9,29 +10,37 @@ from modules.auth.dependencies import admin_only router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(admin_only)]) +# Define a Pydantic model for the request body +class ClearDbRequest(BaseModel): + hard: bool + @router.get("/") def read_admin(): return {"message": "Admin route"} -@router.get("/cleardb") -def clear_db(db: Annotated[Session, Depends(get_db)], hard: bool): +# Change to POST and use the request body model +@router.post("/cleardb") +def clear_db(payload: ClearDbRequest, db: Annotated[Session, Depends(get_db)]): """ - Clear the database. - 'hard' parameter determines if the database should be completely reset. + Clear the database based on the 'hard' flag in the request body. + 'hard'=True: Drop and recreate all tables. + 'hard'=False: Delete data from tables except users. """ + hard = payload.hard # Get 'hard' from the payload if hard: + # ... existing hard clear logic ... Base.metadata.drop_all(bind=db.get_bind()) Base.metadata.create_all(bind=db.get_bind()) + db.commit() return {"message": "Database reset (HARD)"} else: + # ... existing soft clear logic ... tables = Base.metadata.tables.keys() for table_name in tables: # delete all tables that isn't the users table if table_name != "users": table = Base.metadata.tables[table_name] + print(f"Deleting table: {table_name}") db.execute(table.delete()) - - # delete all non-admin accounts - db.query(User).filter(User.role != UserRole.ADMIN).delete() db.commit() return {"message": "Database cleared"} \ No newline at end of file diff --git a/backend/modules/auth/__pycache__/api.cpython-312.pyc b/backend/modules/auth/__pycache__/api.cpython-312.pyc index f8f5808..edff5ae 100644 Binary files a/backend/modules/auth/__pycache__/api.cpython-312.pyc and b/backend/modules/auth/__pycache__/api.cpython-312.pyc differ diff --git a/interfaces/nativeapp/src/api/admin.ts b/interfaces/nativeapp/src/api/admin.ts new file mode 100644 index 0000000..442e144 --- /dev/null +++ b/interfaces/nativeapp/src/api/admin.ts @@ -0,0 +1,16 @@ +import apiClient from './client'; + +interface ClearDbResponse { + message: string; +} + +export const clearDatabase = async (hard: boolean): Promise => { + try { + const response = await apiClient.post('/admin/cleardb', { hard }); + return response.data; + } catch (error) { + console.error('Error calling clearDatabase API:', error); + // Re-throw the error so the component can handle it + throw error; + } +}; diff --git a/interfaces/nativeapp/src/api/client.ts b/interfaces/nativeapp/src/api/client.ts index 616f07c..4cb43c4 100644 --- a/interfaces/nativeapp/src/api/client.ts +++ b/interfaces/nativeapp/src/api/client.ts @@ -5,8 +5,8 @@ import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; -const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.255.221:8000/api'; // Use your machine's IP -// const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.1.9:8000/api'; // Use your machine's IP +// const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.255.221:8000/api'; // Use your machine's IP +const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.1.9:8000/api'; // Use your machine's IP // const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8000/api'; // Use your machine's IP const ACCESS_TOKEN_KEY = 'maia_access_token'; // Renamed for clarity const REFRESH_TOKEN_KEY = 'maia_refresh_token'; // Key for refresh token diff --git a/interfaces/nativeapp/src/components/WebSidebar.tsx b/interfaces/nativeapp/src/components/WebSidebar.tsx index 5998464..2fba952 100644 --- a/interfaces/nativeapp/src/components/WebSidebar.tsx +++ b/interfaces/nativeapp/src/components/WebSidebar.tsx @@ -4,26 +4,38 @@ import { View, StyleSheet, Pressable } from 'react-native'; import { Text, useTheme, Icon } from 'react-native-paper'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import { NavigationContainerRef } from '@react-navigation/native'; // Import ref type -import { RootStackParamList } from '../types/navigation'; // Import stack param list +import { WebContentStackParamList } from '../types/navigation'; // Use WebContentStackParamList +import { useAuth, UserRole } from '../contexts/AuthContext'; // Import useAuth and UserRole // Define Props including the navigation ref interface WebSidebarProps { - navigationRef: React.RefObject>; + navigationRef: React.RefObject>; // Use WebContentStackParamList currentRouteName?: string; // To highlight the active item } +// Define navigation items type +type NavItem = { + name: keyof WebContentStackParamList; + icon: string; + label: string; + adminOnly?: boolean; // Add flag for admin-only items +}; + // Define navigation items -const navItems = [ +const navItems: NavItem[] = [ { name: 'Dashboard', icon: 'view-dashboard', label: 'Dashboard' }, { name: 'Chat', icon: 'chat', label: 'Chat' }, { name: 'Calendar', icon: 'calendar', label: 'Calendar' }, { name: 'Profile', icon: 'account-circle', label: 'Profile' }, -] as const; // Use 'as const' for stricter typing of names + { name: 'Admin', icon: 'shield-crown', label: 'Admin', adminOnly: true }, // Add Admin item +]; const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => { const theme = useTheme(); + const { user } = useAuth(); // Get user data + const isAdmin = user?.role === UserRole.ADMIN; - const handleNavigate = (screenName: keyof RootStackParamList) => { + const handleNavigate = (screenName: keyof WebContentStackParamList) => { // Use the ref to navigate if (navigationRef.current) { navigationRef.current.navigate(screenName); @@ -73,7 +85,7 @@ const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => { color: theme.colors.onPrimary, // Text color on primary background }, inactiveLabel: { - color: theme.colors.textSecondary, // Text color for inactive items + color: theme.colors.onSurfaceVariant, // Text color for inactive items }, }); @@ -85,6 +97,11 @@ const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => { {navItems.map((item) => { + // Skip admin item if user is not admin + if (item.adminOnly && !isAdmin) { + return null; + } + const isActive = currentRouteName === item.name; return ( { Promise; logout: () => Promise; } @@ -15,6 +29,7 @@ interface AuthContextData { const AuthContext = createContext({ isAuthenticated: false, isLoading: true, + user: null, // Initialize user as null login: async () => { throw new Error('AuthContext not initialized'); }, logout: async () => { throw new Error('AuthContext not initialized'); }, }); @@ -26,15 +41,45 @@ interface AuthProviderProps { export const AuthProvider: React.FC = ({ children }) => { const [isLoading, setIsLoading] = useState(true); const [isAuthenticatedState, setIsAuthenticatedState] = useState(false); + const [userState, setUserState] = useState(null); // State for user data + + // Function to fetch user data + const fetchUserData = useCallback(async () => { + try { + console.log("[AuthContext] fetchUserData: Fetching /user/me"); + const response = await apiClient.get('/user/me'); + console.log("[AuthContext] fetchUserData: User data received:", response.data); + setUserState(response.data); + return response.data; + } catch (error: any) { + console.error("[AuthContext] fetchUserData: Error fetching user data:", error.response?.data || error.message); + // If fetching user fails (e.g., token expired mid-session), log out + await clearTokens(); + setIsAuthenticatedState(false); + setUserState(null); + return null; + } + }, []); const checkAuthStatus = useCallback(async () => { const token = await getAccessToken(); const hasToken = !!token; - if (hasToken !== isAuthenticatedState) { - setIsAuthenticatedState(hasToken); + if (hasToken) { + console.log("[AuthContext] checkAuthStatus: Token found, fetching user data."); + const userData = await fetchUserData(); + if (userData) { + setIsAuthenticatedState(true); + } else { + // Fetch failed, already handled logout in fetchUserData + setIsAuthenticatedState(false); + } + } else { + console.log("[AuthContext] checkAuthStatus: No token found."); + setIsAuthenticatedState(false); + setUserState(null); } - return hasToken; - }, [isAuthenticatedState]); + return isAuthenticatedState; // Return the updated state + }, [fetchUserData, isAuthenticatedState]); // Added isAuthenticatedState dependency const loadInitialAuth = useCallback(async () => { setIsLoading(true); @@ -43,9 +88,10 @@ export const AuthProvider: React.FC = ({ children }) => { await checkAuthStatus(); console.log("[AuthContext] loadInitialAuth: Initial check complete."); } catch (error) { - console.error("[AuthContext] loadInitialAuth: Error loading initial token:", error); + console.error("[AuthContext] loadInitialAuth: Error during initial auth check:", error); await clearTokens(); setIsAuthenticatedState(false); + setUserState(null); } finally { setIsLoading(false); } @@ -58,7 +104,7 @@ export const AuthProvider: React.FC = ({ children }) => { const login = useCallback(async (username: string, password: string) => { console.log("[AuthContext] login: Function called with:", username); try { - console.log("[AuthContext] login: Preparing to call apiClient.post for /auth/login"); + // ... (existing login API call) ... const response = await apiClient.post( '/auth/login', 'grant_type=password&username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&scope=&client_id=&client_secret=', @@ -70,10 +116,8 @@ export const AuthProvider: React.FC = ({ children }) => { } ); - console.log("[AuthContext] login: apiClient.post successful, response status:", response?.status); const { access_token, refresh_token } = response.data; - console.log("[AuthContext] login: Response data received."); - + // ... (existing token validation) ... if (!access_token || typeof access_token !== 'string' || !refresh_token) { console.error("[AuthContext] login: Invalid token structure received:", response.data); throw new Error('Invalid token received from server.'); @@ -81,29 +125,30 @@ export const AuthProvider: React.FC = ({ children }) => { console.log('[AuthContext] login: Login successful, storing tokens.'); await storeTokens(access_token, refresh_token); - setIsAuthenticatedState(true); + + // Fetch user data immediately after successful login + const userData = await fetchUserData(); + if (userData) { + setIsAuthenticatedState(true); + } else { + // Should not happen if login succeeded, but handle defensively + throw new Error('Failed to fetch user data after login.'); + } } catch (error: any) { + // ... (existing error handling) ... console.error("[AuthContext] login: Caught Error Object:", error); - if (error.isAxiosError) { - console.error("[AuthContext] login: Axios Error Details:"); - console.error(" Request Config:", error.config); - console.error(" Response:", error.response?.status, error.response?.data); - console.error(" Message:", error.message); - } await clearTokens(); setIsAuthenticatedState(false); + setUserState(null); throw error; } - }, []); + }, [fetchUserData]); // Added fetchUserData dependency const logout = useCallback(async () => { console.log('[AuthContext] logout: Logging out.'); const refreshToken = await getRefreshToken(); - if (!refreshToken) { - console.warn('[AuthContext] logout: No refresh token found to send to backend.'); - } - + // ... (existing backend logout call) ... try { if (refreshToken) { console.log('[AuthContext] logout: Calling backend /auth/logout'); @@ -115,6 +160,7 @@ export const AuthProvider: React.FC = ({ children }) => { } finally { await clearTokens(); setIsAuthenticatedState(false); + setUserState(null); // Clear user data on logout console.log('[AuthContext] logout: Local tokens cleared and state updated.'); } }, []); @@ -122,10 +168,12 @@ export const AuthProvider: React.FC = ({ children }) => { const contextValue = useMemo(() => ({ isAuthenticated: isAuthenticatedState, isLoading, + user: userState, // Provide user state login, logout, - }), [isAuthenticatedState, isLoading, login, logout]); + }), [isAuthenticatedState, isLoading, userState, login, logout]); // Added userState dependency + // ... (rest of the component: Provider, useAuth, AuthLoadingScreen) ... return ( {children} @@ -147,4 +195,7 @@ export const AuthLoadingScreen: React.FC = () => { container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: theme.colors.background } }); return (); -} \ No newline at end of file +} + +// Export UserRole if needed elsewhere +export { UserRole }; \ No newline at end of file diff --git a/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx b/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx index d11c709..8954f12 100644 --- a/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx +++ b/interfaces/nativeapp/src/navigation/MobileTabNavigator.tsx @@ -9,6 +9,8 @@ import ChatScreen from '../screens/ChatScreen'; import CalendarScreen from '../screens/CalendarScreen'; import ProfileScreen from '../screens/ProfileScreen'; import EventFormScreen from '../screens/EventFormScreen'; +import AdminScreen from '../screens/AdminScreen'; // Import AdminScreen +import { useAuth, UserRole } from '../contexts/AuthContext'; // Import useAuth and UserRole import { MobileTabParamList } from '../types/navigation'; @@ -16,13 +18,15 @@ const Tab = createBottomTabNavigator(); const MobileTabNavigator = () => { const theme = useTheme(); + const { user } = useAuth(); // Get user data from context + const isAdmin = user?.role === UserRole.ADMIN; return ( ({ tabBarActiveTintColor: theme.colors.secondary, - tabBarInactiveTintColor: theme.colors.textSecondary, + tabBarInactiveTintColor: theme.colors.onSurfaceVariant, // Use onSurfaceVariant instead of textSecondary tabBarStyle: { backgroundColor: theme.colors.surface, borderTopColor: theme.colors.surface, // Or a subtle border @@ -30,7 +34,7 @@ const MobileTabNavigator = () => { headerStyle: { backgroundColor: theme.colors.surface, }, - headerTintColor: theme.colors.text, + headerTintColor: theme.colors.onSurface, // Use onSurface instead of text tabBarIcon: ({ focused, color, size }) => { let iconName: string = 'help-circle'; // Default icon @@ -42,6 +46,8 @@ const MobileTabNavigator = () => { iconName = focused ? 'calendar' : 'calendar-outline'; } else if (route.name === 'ProfileTab') { iconName = focused ? 'account-circle' : 'account-circle-outline'; + } else if (route.name === 'AdminTab') { // Add icon for AdminTab + iconName = focused ? 'shield-crown' : 'shield-crown-outline'; } return ; }, @@ -67,6 +73,14 @@ const MobileTabNavigator = () => { component={ProfileScreen} options={{ title: 'Profile', headerTitle: 'Profile' }} /> + {/* Conditionally render Admin Tab */} + {isAdmin && ( + + )} ); }; diff --git a/interfaces/nativeapp/src/navigation/WebContentNavigator.tsx b/interfaces/nativeapp/src/navigation/WebContentNavigator.tsx index b61a361..24d049d 100644 --- a/interfaces/nativeapp/src/navigation/WebContentNavigator.tsx +++ b/interfaces/nativeapp/src/navigation/WebContentNavigator.tsx @@ -8,6 +8,8 @@ import ChatScreen from '../screens/ChatScreen'; import CalendarScreen from '../screens/CalendarScreen'; import ProfileScreen from '../screens/ProfileScreen'; import EventFormScreen from '../screens/EventFormScreen'; +import AdminScreen from '../screens/AdminScreen'; // Import AdminScreen +import { useAuth, UserRole } from '../contexts/AuthContext'; // Import useAuth and UserRole import { WebContentStackParamList } from '../types/navigation'; @@ -15,6 +17,8 @@ const Stack = createNativeStackNavigator(); const WebContentNavigator = () => { const theme = useTheme(); + const { user } = useAuth(); // Get user data + const isAdmin = user?.role === UserRole.ADMIN; return ( { headerStyle: { backgroundColor: theme.colors.surface, }, - headerTintColor: theme.colors.text, + headerTintColor: theme.colors.onSurface, // Use onSurface instead of text headerTitleStyle: { fontWeight: 'bold', }, @@ -39,6 +43,10 @@ const WebContentNavigator = () => { + {/* Conditionally add Admin screen */} + {isAdmin && ( + + )} ); }; diff --git a/interfaces/nativeapp/src/screens/AdminScreen.tsx b/interfaces/nativeapp/src/screens/AdminScreen.tsx new file mode 100644 index 0000000..9785771 --- /dev/null +++ b/interfaces/nativeapp/src/screens/AdminScreen.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Button, Checkbox, Text, ActivityIndicator, Snackbar } from 'react-native-paper'; +import { clearDatabase } from '../api/admin'; // Revert to standard import without extension + +const AdminScreen = () => { + const [isHardClear, setIsHardClear] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [snackbarVisible, setSnackbarVisible] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + + const handleClearDb = async () => { + setIsLoading(true); + setSnackbarVisible(false); + try { + const response = await clearDatabase(isHardClear); + setSnackbarMessage(response.message || 'Database cleared successfully.'); + setSnackbarVisible(true); + } catch (error: any) { + console.error("Error clearing database:", error); + setSnackbarMessage(error.response?.data?.detail || 'Failed to clear database.'); + setSnackbarVisible(true); + } finally { + setIsLoading(false); + } + }; + + return ( + + Admin Controls + + + setIsHardClear(!isHardClear)} + /> + setIsHardClear(!isHardClear)}>Hard Clear (Delete all data) + + + + + setSnackbarVisible(false)} + duration={Snackbar.DURATION_SHORT} + > + {snackbarMessage} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 20, + justifyContent: 'center', + alignItems: 'center', + }, + title: { + marginBottom: 30, + }, + checkboxContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 20, + }, + button: { + marginTop: 10, + }, +}); + +export default AdminScreen; diff --git a/interfaces/nativeapp/src/types/navigation.ts b/interfaces/nativeapp/src/types/navigation.ts index 7213f67..27fd3eb 100644 --- a/interfaces/nativeapp/src/types/navigation.ts +++ b/interfaces/nativeapp/src/types/navigation.ts @@ -7,6 +7,7 @@ export type MobileTabParamList = { ChatTab: undefined; CalendarTab: undefined; ProfileTab: undefined; + AdminTab?: undefined; // Add Admin tab (optional) }; // Screens within the Web Content Area Stack Navigator @@ -15,6 +16,7 @@ export type WebContentStackParamList = { Chat: undefined; Calendar: undefined; Profile: undefined; + Admin?: undefined; // Add Admin screen EventForm?: { eventId?: number; selectedDate?: string }; };