Added admin page
This commit is contained in:
Binary file not shown.
@@ -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"}
|
||||||
Binary file not shown.
16
interfaces/nativeapp/src/api/admin.ts
Normal file
16
interfaces/nativeapp/src/api/admin.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
81
interfaces/nativeapp/src/screens/AdminScreen.tsx
Normal file
81
interfaces/nativeapp/src/screens/AdminScreen.tsx
Normal 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;
|
||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user