Added admin page

This commit is contained in:
c-d-p
2025-04-22 00:10:57 +02:00
parent c0a58b45f4
commit bf147af3ef
11 changed files with 241 additions and 43 deletions

View File

@@ -1,6 +1,7 @@
# modules/admin/api.py # modules/admin/api.py
from typing import Annotated 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 sqlalchemy.orm import Session
from core.database import Base, get_db from core.database import Base, get_db
from modules.auth.models import User, UserRole 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)]) 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("/") @router.get("/")
def read_admin(): def read_admin():
return {"message": "Admin route"} return {"message": "Admin route"}
@router.get("/cleardb") # Change to POST and use the request body model
def clear_db(db: Annotated[Session, Depends(get_db)], hard: bool): @router.post("/cleardb")
def clear_db(payload: ClearDbRequest, db: Annotated[Session, Depends(get_db)]):
""" """
Clear the database. Clear the database based on the 'hard' flag in the request body.
'hard' parameter determines if the database should be completely reset. '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: if hard:
# ... existing hard clear logic ...
Base.metadata.drop_all(bind=db.get_bind()) Base.metadata.drop_all(bind=db.get_bind())
Base.metadata.create_all(bind=db.get_bind()) Base.metadata.create_all(bind=db.get_bind())
db.commit()
return {"message": "Database reset (HARD)"} return {"message": "Database reset (HARD)"}
else: else:
# ... existing soft clear logic ...
tables = Base.metadata.tables.keys() tables = Base.metadata.tables.keys()
for table_name in tables: for table_name in tables:
# delete all tables that isn't the users table # delete all tables that isn't the users table
if table_name != "users": if table_name != "users":
table = Base.metadata.tables[table_name] table = Base.metadata.tables[table_name]
print(f"Deleting table: {table_name}")
db.execute(table.delete()) db.execute(table.delete())
# delete all non-admin accounts
db.query(User).filter(User.role != UserRole.ADMIN).delete()
db.commit() db.commit()
return {"message": "Database cleared"} return {"message": "Database cleared"}

View File

@@ -0,0 +1,16 @@
import apiClient from './client';
interface ClearDbResponse {
message: string;
}
export const clearDatabase = async (hard: boolean): Promise<ClearDbResponse> => {
try {
const response = await apiClient.post<ClearDbResponse>('/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;
}
};

View File

@@ -5,8 +5,8 @@ import * as SecureStore from 'expo-secure-store';
import AsyncStorage from '@react-native-async-storage/async-storage'; 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.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.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 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 ACCESS_TOKEN_KEY = 'maia_access_token'; // Renamed for clarity
const REFRESH_TOKEN_KEY = 'maia_refresh_token'; // Key for refresh token const REFRESH_TOKEN_KEY = 'maia_refresh_token'; // Key for refresh token

View File

@@ -4,26 +4,38 @@ import { View, StyleSheet, Pressable } from 'react-native';
import { Text, useTheme, Icon } from 'react-native-paper'; import { Text, useTheme, Icon } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { NavigationContainerRef } from '@react-navigation/native'; // Import ref type 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 // Define Props including the navigation ref
interface WebSidebarProps { interface WebSidebarProps {
navigationRef: React.RefObject<NavigationContainerRef<RootStackParamList>>; navigationRef: React.RefObject<NavigationContainerRef<WebContentStackParamList>>; // Use WebContentStackParamList
currentRouteName?: string; // To highlight the active item 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 // Define navigation items
const navItems = [ const navItems: NavItem[] = [
{ name: 'Dashboard', icon: 'view-dashboard', label: 'Dashboard' }, { name: 'Dashboard', icon: 'view-dashboard', label: 'Dashboard' },
{ name: 'Chat', icon: 'chat', label: 'Chat' }, { name: 'Chat', icon: 'chat', label: 'Chat' },
{ name: 'Calendar', icon: 'calendar', label: 'Calendar' }, { name: 'Calendar', icon: 'calendar', label: 'Calendar' },
{ name: 'Profile', icon: 'account-circle', label: 'Profile' }, { 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 WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => {
const theme = useTheme(); 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 // Use the ref to navigate
if (navigationRef.current) { if (navigationRef.current) {
navigationRef.current.navigate(screenName); navigationRef.current.navigate(screenName);
@@ -73,7 +85,7 @@ const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => {
color: theme.colors.onPrimary, // Text color on primary background color: theme.colors.onPrimary, // Text color on primary background
}, },
inactiveLabel: { 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) => {
</View> </View>
{navItems.map((item) => { {navItems.map((item) => {
// Skip admin item if user is not admin
if (item.adminOnly && !isAdmin) {
return null;
}
const isActive = currentRouteName === item.name; const isActive = currentRouteName === item.name;
return ( return (
<Pressable <Pressable
@@ -98,7 +115,7 @@ const WebSidebar = ({ navigationRef, currentRouteName }: WebSidebarProps) => {
<MaterialCommunityIcons <MaterialCommunityIcons
name={item.icon} name={item.icon}
size={24} size={24}
color={isActive ? theme.colors.onPrimary : theme.colors.textSecondary} color={isActive ? theme.colors.onPrimary : theme.colors.onSurfaceVariant}
style={styles.icon} style={styles.icon}
/> />
<Text <Text

View File

@@ -5,9 +5,23 @@ import apiClient from '../api/client';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import { getAccessToken, getRefreshToken, storeTokens, clearTokens } from '../api/client'; import { getAccessToken, getRefreshToken, storeTokens, clearTokens } from '../api/client';
// Define UserRole enum matching the backend
enum UserRole {
USER = 'user',
ADMIN = 'admin',
}
interface UserData {
username: string;
name: string;
role: UserRole;
// Add other user fields if needed
}
interface AuthContextData { interface AuthContextData {
isAuthenticated: boolean; isAuthenticated: boolean;
isLoading: boolean; isLoading: boolean;
user: UserData | null; // Add user data to context
login: (username: string, password: string) => Promise<void>; login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
} }
@@ -15,6 +29,7 @@ interface AuthContextData {
const AuthContext = createContext<AuthContextData>({ const AuthContext = createContext<AuthContextData>({
isAuthenticated: false, isAuthenticated: false,
isLoading: true, isLoading: true,
user: null, // Initialize user as null
login: async () => { throw new Error('AuthContext not initialized'); }, login: async () => { throw new Error('AuthContext not initialized'); },
logout: 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<AuthProviderProps> = ({ children }) => { export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [isAuthenticatedState, setIsAuthenticatedState] = useState<boolean>(false); const [isAuthenticatedState, setIsAuthenticatedState] = useState<boolean>(false);
const [userState, setUserState] = useState<UserData | null>(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<UserData>('/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 checkAuthStatus = useCallback(async () => {
const token = await getAccessToken(); const token = await getAccessToken();
const hasToken = !!token; const hasToken = !!token;
if (hasToken !== isAuthenticatedState) { if (hasToken) {
setIsAuthenticatedState(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; return isAuthenticatedState; // Return the updated state
}, [isAuthenticatedState]); }, [fetchUserData, isAuthenticatedState]); // Added isAuthenticatedState dependency
const loadInitialAuth = useCallback(async () => { const loadInitialAuth = useCallback(async () => {
setIsLoading(true); setIsLoading(true);
@@ -43,9 +88,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
await checkAuthStatus(); await checkAuthStatus();
console.log("[AuthContext] loadInitialAuth: Initial check complete."); console.log("[AuthContext] loadInitialAuth: Initial check complete.");
} catch (error) { } catch (error) {
console.error("[AuthContext] loadInitialAuth: Error loading initial token:", error); console.error("[AuthContext] loadInitialAuth: Error during initial auth check:", error);
await clearTokens(); await clearTokens();
setIsAuthenticatedState(false); setIsAuthenticatedState(false);
setUserState(null);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -58,7 +104,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const login = useCallback(async (username: string, password: string) => { const login = useCallback(async (username: string, password: string) => {
console.log("[AuthContext] login: Function called with:", username); console.log("[AuthContext] login: Function called with:", username);
try { try {
console.log("[AuthContext] login: Preparing to call apiClient.post for /auth/login"); // ... (existing login API call) ...
const response = await apiClient.post( const response = await apiClient.post(
'/auth/login', '/auth/login',
'grant_type=password&username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&scope=&client_id=&client_secret=', 'grant_type=password&username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password) + '&scope=&client_id=&client_secret=',
@@ -70,10 +116,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
} }
); );
console.log("[AuthContext] login: apiClient.post successful, response status:", response?.status);
const { access_token, refresh_token } = response.data; 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) { if (!access_token || typeof access_token !== 'string' || !refresh_token) {
console.error("[AuthContext] login: Invalid token structure received:", response.data); console.error("[AuthContext] login: Invalid token structure received:", response.data);
throw new Error('Invalid token received from server.'); throw new Error('Invalid token received from server.');
@@ -81,29 +125,30 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
console.log('[AuthContext] login: Login successful, storing tokens.'); console.log('[AuthContext] login: Login successful, storing tokens.');
await storeTokens(access_token, refresh_token); 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) { } catch (error: any) {
// ... (existing error handling) ...
console.error("[AuthContext] login: Caught Error Object:", error); 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(); await clearTokens();
setIsAuthenticatedState(false); setIsAuthenticatedState(false);
setUserState(null);
throw error; throw error;
} }
}, []); }, [fetchUserData]); // Added fetchUserData dependency
const logout = useCallback(async () => { const logout = useCallback(async () => {
console.log('[AuthContext] logout: Logging out.'); console.log('[AuthContext] logout: Logging out.');
const refreshToken = await getRefreshToken(); const refreshToken = await getRefreshToken();
if (!refreshToken) { // ... (existing backend logout call) ...
console.warn('[AuthContext] logout: No refresh token found to send to backend.');
}
try { try {
if (refreshToken) { if (refreshToken) {
console.log('[AuthContext] logout: Calling backend /auth/logout'); console.log('[AuthContext] logout: Calling backend /auth/logout');
@@ -115,6 +160,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
} finally { } finally {
await clearTokens(); await clearTokens();
setIsAuthenticatedState(false); setIsAuthenticatedState(false);
setUserState(null); // Clear user data on logout
console.log('[AuthContext] logout: Local tokens cleared and state updated.'); console.log('[AuthContext] logout: Local tokens cleared and state updated.');
} }
}, []); }, []);
@@ -122,10 +168,12 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const contextValue = useMemo(() => ({ const contextValue = useMemo(() => ({
isAuthenticated: isAuthenticatedState, isAuthenticated: isAuthenticatedState,
isLoading, isLoading,
user: userState, // Provide user state
login, login,
logout, logout,
}), [isAuthenticatedState, isLoading, login, logout]); }), [isAuthenticatedState, isLoading, userState, login, logout]); // Added userState dependency
// ... (rest of the component: Provider, useAuth, AuthLoadingScreen) ...
return ( return (
<AuthContext.Provider value={contextValue}> <AuthContext.Provider value={contextValue}>
{children} {children}
@@ -147,4 +195,7 @@ export const AuthLoadingScreen: React.FC = () => {
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: theme.colors.background } container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: theme.colors.background }
}); });
return (<View style={styles.container}><ActivityIndicator size="large" color={theme.colors.primary} /></View>); return (<View style={styles.container}><ActivityIndicator size="large" color={theme.colors.primary} /></View>);
} }
// Export UserRole if needed elsewhere
export { UserRole };

View File

@@ -9,6 +9,8 @@ import ChatScreen from '../screens/ChatScreen';
import CalendarScreen from '../screens/CalendarScreen'; import CalendarScreen from '../screens/CalendarScreen';
import ProfileScreen from '../screens/ProfileScreen'; import ProfileScreen from '../screens/ProfileScreen';
import EventFormScreen from '../screens/EventFormScreen'; 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'; import { MobileTabParamList } from '../types/navigation';
@@ -16,13 +18,15 @@ const Tab = createBottomTabNavigator<MobileTabParamList>();
const MobileTabNavigator = () => { const MobileTabNavigator = () => {
const theme = useTheme(); const theme = useTheme();
const { user } = useAuth(); // Get user data from context
const isAdmin = user?.role === UserRole.ADMIN;
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName="DashboardTab" initialRouteName="DashboardTab"
screenOptions={({ route }) => ({ screenOptions={({ route }) => ({
tabBarActiveTintColor: theme.colors.secondary, tabBarActiveTintColor: theme.colors.secondary,
tabBarInactiveTintColor: theme.colors.textSecondary, tabBarInactiveTintColor: theme.colors.onSurfaceVariant, // Use onSurfaceVariant instead of textSecondary
tabBarStyle: { tabBarStyle: {
backgroundColor: theme.colors.surface, backgroundColor: theme.colors.surface,
borderTopColor: theme.colors.surface, // Or a subtle border borderTopColor: theme.colors.surface, // Or a subtle border
@@ -30,7 +34,7 @@ const MobileTabNavigator = () => {
headerStyle: { headerStyle: {
backgroundColor: theme.colors.surface, backgroundColor: theme.colors.surface,
}, },
headerTintColor: theme.colors.text, headerTintColor: theme.colors.onSurface, // Use onSurface instead of text
tabBarIcon: ({ focused, color, size }) => { tabBarIcon: ({ focused, color, size }) => {
let iconName: string = 'help-circle'; // Default icon let iconName: string = 'help-circle'; // Default icon
@@ -42,6 +46,8 @@ const MobileTabNavigator = () => {
iconName = focused ? 'calendar' : 'calendar-outline'; iconName = focused ? 'calendar' : 'calendar-outline';
} else if (route.name === 'ProfileTab') { } else if (route.name === 'ProfileTab') {
iconName = focused ? 'account-circle' : 'account-circle-outline'; iconName = focused ? 'account-circle' : 'account-circle-outline';
} else if (route.name === 'AdminTab') { // Add icon for AdminTab
iconName = focused ? 'shield-crown' : 'shield-crown-outline';
} }
return <MaterialCommunityIcons name={iconName} size={size} color={color} />; return <MaterialCommunityIcons name={iconName} size={size} color={color} />;
}, },
@@ -67,6 +73,14 @@ const MobileTabNavigator = () => {
component={ProfileScreen} component={ProfileScreen}
options={{ title: 'Profile', headerTitle: 'Profile' }} options={{ title: 'Profile', headerTitle: 'Profile' }}
/> />
{/* Conditionally render Admin Tab */}
{isAdmin && (
<Tab.Screen
name="AdminTab"
component={AdminScreen}
options={{ title: 'Admin', headerTitle: 'Admin Controls' }}
/>
)}
</Tab.Navigator> </Tab.Navigator>
); );
}; };

View File

@@ -8,6 +8,8 @@ import ChatScreen from '../screens/ChatScreen';
import CalendarScreen from '../screens/CalendarScreen'; import CalendarScreen from '../screens/CalendarScreen';
import ProfileScreen from '../screens/ProfileScreen'; import ProfileScreen from '../screens/ProfileScreen';
import EventFormScreen from '../screens/EventFormScreen'; 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'; import { WebContentStackParamList } from '../types/navigation';
@@ -15,6 +17,8 @@ const Stack = createNativeStackNavigator<WebContentStackParamList>();
const WebContentNavigator = () => { const WebContentNavigator = () => {
const theme = useTheme(); const theme = useTheme();
const { user } = useAuth(); // Get user data
const isAdmin = user?.role === UserRole.ADMIN;
return ( return (
<Stack.Navigator <Stack.Navigator
@@ -23,7 +27,7 @@ const WebContentNavigator = () => {
headerStyle: { headerStyle: {
backgroundColor: theme.colors.surface, backgroundColor: theme.colors.surface,
}, },
headerTintColor: theme.colors.text, headerTintColor: theme.colors.onSurface, // Use onSurface instead of text
headerTitleStyle: { headerTitleStyle: {
fontWeight: 'bold', fontWeight: 'bold',
}, },
@@ -39,6 +43,10 @@ const WebContentNavigator = () => {
<Stack.Screen name="Calendar" component={CalendarScreen} /> <Stack.Screen name="Calendar" component={CalendarScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} /> <Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="EventForm" component={EventFormScreen} /> <Stack.Screen name="EventForm" component={EventFormScreen} />
{/* Conditionally add Admin screen */}
{isAdmin && (
<Stack.Screen name="Admin" component={AdminScreen} options={{ title: 'Admin Controls' }}/>
)}
</Stack.Navigator> </Stack.Navigator>
); );
}; };

View File

@@ -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 (
<View style={styles.container}>
<Text variant="headlineMedium" style={styles.title}>Admin Controls</Text>
<View style={styles.checkboxContainer}>
<Checkbox
status={isHardClear ? 'checked' : 'unchecked'}
onPress={() => setIsHardClear(!isHardClear)}
/>
<Text onPress={() => setIsHardClear(!isHardClear)}>Hard Clear (Delete all data)</Text>
</View>
<Button
mode="contained"
onPress={handleClearDb}
disabled={isLoading}
style={styles.button}
buttonColor="red" // Make it look dangerous
>
{isLoading ? <ActivityIndicator animating={true} color="white" /> : 'Clear Database'}
</Button>
<Snackbar
visible={snackbarVisible}
onDismiss={() => setSnackbarVisible(false)}
duration={Snackbar.DURATION_SHORT}
>
{snackbarMessage}
</Snackbar>
</View>
);
};
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;

View File

@@ -7,6 +7,7 @@ export type MobileTabParamList = {
ChatTab: undefined; ChatTab: undefined;
CalendarTab: undefined; CalendarTab: undefined;
ProfileTab: undefined; ProfileTab: undefined;
AdminTab?: undefined; // Add Admin tab (optional)
}; };
// Screens within the Web Content Area Stack Navigator // Screens within the Web Content Area Stack Navigator
@@ -15,6 +16,7 @@ export type WebContentStackParamList = {
Chat: undefined; Chat: undefined;
Calendar: undefined; Calendar: undefined;
Profile: undefined; Profile: undefined;
Admin?: undefined; // Add Admin screen
EventForm?: { eventId?: number; selectedDate?: string }; EventForm?: { eventId?: number; selectedDate?: string };
}; };