+
{name} Dashboard - Coming Soon
);
+
function App() {
+ // 1. Pull the initialization function from Zustand
+ const checkAuth = useAuthStore((state) => state.checkAuth);
+
+ // 2. Ping Django to verify the HttpOnly cookie the moment the app boots
+ useEffect(() => {
+ checkAuth();
+ }, [checkAuth]);
+
return (
- {/*Public Routes*/}
+ {/* --- Public Routes --- */}
} />
- {/*Non-Public Routes*/}
- }>
+ {/* --- SECURE ROUTES --- */}
+ }>
- }>
- {/* We use our "pass" component to fill the Outlet until the real pages are built */}
- } />
- } />
- } />
- } />
- } />
+ {/* --- Non-Public Routes (Now completely locked down) --- */}
+ }>
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ } />
+ } />
+ } />
+ } />
- } />
- } />
- } />
- } />
- {/*Redirect Routes*/}
+ {/* --- Redirect Routes --- */}
} />
-
- );
+
+ );
}
-export default App
\ No newline at end of file
+export default App;
\ No newline at end of file
diff --git a/client/src/api/axiosConfig.js b/client/src/api/axiosConfig.js
new file mode 100644
index 0000000..c8dec25
--- /dev/null
+++ b/client/src/api/axiosConfig.js
@@ -0,0 +1,17 @@
+// src/api/axiosConfig.js
+import axios from 'axios';
+
+const api = axios.create({
+ // Vite will catch this relative path and proxy it to Django
+ baseURL: '/api/v3/',
+
+ withCredentials: true,
+ xsrfCookieName: 'csrftoken',
+ xsrfHeaderName: 'X-CSRFToken',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+});
+
+export default api;
\ No newline at end of file
diff --git a/client/src/components/Widgets/VaaActivity7Day.jsx b/client/src/components/Widgets/VaaActivity7Day.jsx
index 9742756..62e151a 100644
--- a/client/src/components/Widgets/VaaActivity7Day.jsx
+++ b/client/src/components/Widgets/VaaActivity7Day.jsx
@@ -59,12 +59,12 @@ export default function VaaActivity7Day() {
paper_bgcolor: 'transparent', // Let Chakra UI background show through
plot_bgcolor: 'transparent',
font: { color: '#d1d5db' }, // gray.300
- // margin: { t: 40, r: 20, l: 30, b: 60 }, // Extra bottom margin for legend
- title: {
- text: '7 Day Consolidated Flight Levels',
- font: { size: 16, color: 'white' },
- x: 0.05
- },
+ margin: { t: 20, r: 20, l: 20, b: 20 },
+ // title: {
+ // text: '7 Day Consolidated Flight Levels',
+ // font: { size: 16, color: 'white' },
+ // x: 0.05
+ // },
xaxis: {
title: {
text: 'Date (UTC)',
diff --git a/client/src/components/layout/Header.jsx b/client/src/components/layout/Header.jsx
index 819859c..47adb32 100644
--- a/client/src/components/layout/Header.jsx
+++ b/client/src/components/layout/Header.jsx
@@ -1,22 +1,22 @@
import { useRecipe, Button, Stack, Text, Flex } from "@chakra-ui/react"
import UserAvatarMenu from "./UserAvatarMenu.jsx";
import { NavLink } from "react-router-dom";
+import useAuthStore from "@/store/authStore";
// Import the asset properly so Vite can bundle it for any server
// import defaultAvatar from "@/assets/user_profile.svg";
-// TODO: Pull this dynamically from an Auth Context later!
-const placeholderUser = {
- name: "Princess Zelda",
- email: "zelda.royal@hyrule.gov",
- // avatar: defaultAvatar
-}
-
// Accept a 'user' prop now, fallback to placeholder if empty
-export default function Header({ user = placeholderUser }) {
+export default function Header() {
const recipe = useRecipe({ key: "header" });
const styles = recipe();
+ const { user } = useAuthStore();
+ console.log(user);
+ const displayName = user ? (user.fullName || user.username) : "Loading...";
+
+ const displayEmail = user?.email || "LDAP User";
+
return (
{/* Home Link Logo */}
@@ -27,12 +27,12 @@ export default function Header({ user = placeholderUser }) {
{/* User Profile Section */}
- {user.name}
- {user.email}
+ {displayName}
+ {displayEmail}
diff --git a/client/src/components/layout/ProtectedRoute.jsx b/client/src/components/layout/ProtectedRoute.jsx
new file mode 100644
index 0000000..66ddfe3
--- /dev/null
+++ b/client/src/components/layout/ProtectedRoute.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Navigate, Outlet } from 'react-router-dom';
+import useAuthStore from '../../store/authStore';
+
+const ProtectedRoute = () => {
+ // Grab the state directly from your Zustand store
+ const { isAuthenticated, isLoading } = useAuthStore();
+
+ // 1. App is currently pinging Django to verify the HttpOnly cookie
+ if (isLoading) {
+ return (
+
+
Loading Secure Environment...
+
+ );
+ }
+
+ // 2. The check failed (no cookie or expired cookie)
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ // 3. The check passed! Render the layout shell and the protected children
+ return ;
+};
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/client/src/components/layout/UserAvatarMenu.jsx b/client/src/components/layout/UserAvatarMenu.jsx
index 7741183..023242c 100644
--- a/client/src/components/layout/UserAvatarMenu.jsx
+++ b/client/src/components/layout/UserAvatarMenu.jsx
@@ -1,16 +1,18 @@
import { Avatar, Menu, Portal, Box } from "@chakra-ui/react";
import { NavLink } from "react-router-dom";
-
+import useAuthStore from "../../store/authStore";
export default function UserAvatarMenu({ name, avatar }) {
+ const { logout } = useAuthStore();
+
return (
@@ -20,17 +22,24 @@ export default function UserAvatarMenu({ name, avatar }) {
{/* zIndex: overlay = 1300 */}
-
+
Settings
- console.log("Log out triggered")}>
+
+ {/* Replaced console.log with the Zustand logout action */}
+
Logout
- );
+ );
}
\ No newline at end of file
diff --git a/client/src/constants/widgetRegistry.jsx b/client/src/constants/widgetRegistry.jsx
index 0f86332..43d5f77 100644
--- a/client/src/constants/widgetRegistry.jsx
+++ b/client/src/constants/widgetRegistry.jsx
@@ -1,4 +1,5 @@
// config/widgetRegistry.js
+import { lazy } from 'react';
// We assume a 12-column grid.
// w: 12 = full screen width
@@ -11,6 +12,7 @@ export const WIDGET_REGISTRY = {
name: "VAA 7 Day Activity",
category: "Aviation",
description: "Displays 7-day flight level timelines across multiple volcanoes.",
+ component: lazy(() => import('../components/widgets/VaaActivity7Day')),
allowedSizes: {
xlarge: { w: 12, h: 12 },
}
diff --git a/client/src/pages/auth/Login.jsx b/client/src/pages/auth/Login.jsx
index 6f71d4c..b76b172 100644
--- a/client/src/pages/auth/Login.jsx
+++ b/client/src/pages/auth/Login.jsx
@@ -1,91 +1,134 @@
-import { useContext } from 'react';
-import { useForm } from 'react-hook-form';
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
import {
- Button,
- Field,
- Input,
- Stack,
- useRecipe,
- Center,
- Text,
- Flex,
- Heading,
- Highlight,
- GridItem,
- Grid
-} from "@chakra-ui/react"
-import { PasswordInput } from "@/components/ui/password-input.jsx";
+ Box, Heading, Stack, Input, Button, Text, Separator, Field, Link, Flex
+} from '@chakra-ui/react';
+import useAuthStore from '../../store/authStore';
+import { toaster } from "../../components/ui/toaster.jsx"; // Ensure this path matches your project
-export default function Login( signedIn, setSignedIn) {
- const recipe = useRecipe({ key: "dashboard" }); // styling recipes option
- const styles = recipe(); // use style
+const Login = () => {
+ const navigate = useNavigate();
- const {
- register,
- handleSubmit,
- formState: { errors, isSubmitSuccessful },
- } = useForm({
- defaultValues: {
- username: "",
- password: "",
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+
+ const { login, isAuthenticated, isLoading, error } = useAuthStore();
+
+ // --- VDAP COLOR STYLES ---
+ const fieldStyles = {
+ borderRadius: "3xl",
+ bg: "gray.100",
+ borderColor: "gray.100",
+ px: 4,
+ _focus: {
+ bg: "white",
+ borderColor: "blue.400",
+ boxShadow: "0 0 0 1px #4299E1",
+ outline: "none"
}
- });
+ };
- const onSubmit = (data) => {
- console.log(data);
- // setUser(data.username);
+ const buttonStyles = {
+ borderRadius: "3xl",
+ bg: "blue.600",
+ color: "white",
+ _hover: { bg: 'blue.700', transform: 'translateY(-1px)' },
+ _active: { bg: 'blue.800', transform: 'translateY(0)' },
+ transition: 'all 0.2s'
+ };
+
+ // The Redirect Watcher
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate('/home', { replace: true });
+ }
+ }, [isAuthenticated, navigate]);
+
+ // The Error Watcher
+ useEffect(() => {
+ if (error) {
+ toaster.create({
+ title: "Login Failed",
+ description: error,
+ type: "error",
+ duration: 5000,
+ });
+ }
+ }, [error]);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (username && password) {
+ await login(username, password);
+ }
};
return (
-
-
-
-
-
-
- VDAP
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Username
+
+ setUsername(e.target.value)}
+ placeholder="jdoe"
+ {...fieldStyles}
+ />
+
+
+
+
+
+ Password
+
+
+ setPassword(e.target.value)}
+ placeholder="••••••••"
+ {...fieldStyles}
+ />
+
+
+
+
+
+ Need access?{' '}
+
+ Contact IT Administration
+
+
+
+
+
);
-}
\ No newline at end of file
+};
+
+export default Login;
\ No newline at end of file
diff --git a/client/src/pages/home/tabs/MyDashboard.jsx b/client/src/pages/home/tabs/MyDashboard.jsx
index 17ea184..be7d108 100644
--- a/client/src/pages/home/tabs/MyDashboard.jsx
+++ b/client/src/pages/home/tabs/MyDashboard.jsx
@@ -1,94 +1,146 @@
-import { Box, Text, Flex, IconButton } from "@chakra-ui/react";
+import { Box, Text, Flex, IconButton, Spinner } from "@chakra-ui/react";
import { X } from "lucide-react";
import { Responsive, WidthProvider } from "react-grid-layout";
import { useDashboardStore } from "@/store/useDashboardStore.jsx";
+import { Suspense } from 'react';
+
+// 2. Import registry!
+import { WIDGET_REGISTRY } from '@/constants/widgetRegistry.jsx';
// React-Grid-Layout requires this wrapper to automatically calculate screen width
const ResponsiveGridLayout = WidthProvider(Responsive);
export default function MyDashboard() {
- // 1. ☁️ Connect to the Cloud (Zustand)
- const widgets = useDashboardStore(state => state.widgets);
- const isEditing = useDashboardStore(state => state.isEditing);
- const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
- const removeWidget = useDashboardStore(state => state.removeWidget);
+ // 1. ☁️ Connect to the Cloud (Zustand)
+ const widgets = useDashboardStore(state => state.widgets);
+ const isEditing = useDashboardStore(state => state.isEditing);
+ const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
+ const removeWidget = useDashboardStore(state => state.removeWidget);
- // 2. 🛠️ Format the data for RGL
- // RGL expects a very specific array format for its layout prop
- const rglLayout = widgets.map(w => ({
- i: w.id,
- x: w.x,
- y: w.y,
- w: w.w,
- h: w.h,
- isResizable: false // 🚫 The magic lock! No resizing allowed globally.
- }));
+ // 2. 🛠️ Format the data for RGL
+ // RGL expects a very specific array format for its layout prop
+ const rglLayout = widgets.map(w => ({
+ i: w.id,
+ x: w.x,
+ y: w.y,
+ w: w.w,
+ h: w.h,
+ isResizable: false // 🚫 The magic lock! No resizing allowed globally.
+ }));
+
+ // 3. THE COMPONENT MAPPER
+ const renderWidget = (widget) => {
+ const registryEntry = WIDGET_REGISTRY[widget.type];
+
+ if (registryEntry && registryEntry.component) {
+ const Component = registryEntry.component;
+
+ return (
+ // The "Frame" that holds both the Title and the Chart
+