diff --git a/docker-compose.yml b/docker-compose.yml
index 79bc52c..aa483ab 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,8 +6,8 @@ services:
container_name: traceability_web
restart: always
networks:
- - caddy_default
+ - caddy_network
networks:
- caddy_default:
+ caddy_network:
external: true
diff --git a/src/App.tsx b/src/App.tsx
index 009ea58..2b05070 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,12 +3,15 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
+import { AuthProvider } from "@/contexts/AuthContext";
+import { ProtectedRoute } from "@/components/ProtectedRoute";
import Dashboard from "./pages/Dashboard";
import DocumentationPage from "./pages/DocumentationPage";
import AnalysisPage from "./pages/AnalysisPage";
import ALMTypePage from "./pages/ALMTypePage";
import TraceabilityMatrixPage from "./pages/TraceabilityMatrixPage";
import ESPIDFHelperPage from "./pages/ESPIDFHelperPage";
+import LoginPage from "./pages/LoginPage";
import NotFound from "./pages/NotFound";
const queryClient = new QueryClient();
@@ -19,16 +22,61 @@ const App = () => (
-
- } />
- } />
- } />
- } />
- } />
- } />
- {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
- } />
-
+
+
+ } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
+ } />
+
+
diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx
new file mode 100644
index 0000000..20eea9b
--- /dev/null
+++ b/src/components/ProtectedRoute.tsx
@@ -0,0 +1,29 @@
+import { Navigate, useLocation } from "react-router-dom";
+import { useAuth } from "@/contexts/AuthContext";
+import { Loader2 } from "lucide-react";
+
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+}
+
+export function ProtectedRoute({ children }: ProtectedRouteProps) {
+ const { isAuthenticated, isLoading } = useAuth();
+ const location = useLocation();
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return <>{children}>;
+}
diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx
index ea43a22..1b36d06 100644
--- a/src/components/layout/AppLayout.tsx
+++ b/src/components/layout/AppLayout.tsx
@@ -1,10 +1,20 @@
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "./AppSidebar";
import { ThemeToggle } from "@/components/ThemeToggle";
-import { RefreshCw, Clock } from "lucide-react";
+import { RefreshCw, Clock, LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
+import { useAuth } from "@/contexts/AuthContext";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
interface AppLayoutProps {
children: React.ReactNode;
@@ -19,6 +29,12 @@ export function AppLayout({
onRefresh,
isRefreshing = false,
}: AppLayoutProps) {
+ const { user, logout } = useAuth();
+
+ const userInitials = user?.username
+ ? user.username.slice(0, 2).toUpperCase()
+ : "U";
+
return (
@@ -55,6 +71,34 @@ export function AppLayout({
)}
+
+ {/* User Menu */}
+
+
+
+
+
+
+
+
{user?.username}
+
+ {user?.email}
+
+
+
+
+
+
+ Log out
+
+
+
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..5274618
--- /dev/null
+++ b/src/contexts/AuthContext.tsx
@@ -0,0 +1,102 @@
+import React, { createContext, useContext, useState, useEffect, ReactNode } from "react";
+
+interface User {
+ id: number;
+ username: string;
+ email: string;
+ is_active: boolean;
+ is_admin: boolean;
+ created_at: string;
+ updated_at: string;
+}
+
+interface AuthContextType {
+ user: User | null;
+ isAuthenticated: boolean;
+ isLoading: boolean;
+ login: (username: string, password: string) => Promise<{ success: boolean; message: string }>;
+ logout: () => void;
+}
+
+const AuthContext = createContext(undefined);
+
+const SSO_API_URL = "https://sso.nabd-co.com/verify";
+const SSO_API_KEY = "yPkNLCYNm7-UrSZtr_hi-oCx6LZ1DQFAKTGNOoCiMic";
+
+interface AuthProviderProps {
+ children: ReactNode;
+}
+
+export function AuthProvider({ children }: AuthProviderProps) {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ // Check for existing session on mount
+ const storedUser = localStorage.getItem("auth_user");
+ if (storedUser) {
+ try {
+ setUser(JSON.parse(storedUser));
+ } catch {
+ localStorage.removeItem("auth_user");
+ }
+ }
+ setIsLoading(false);
+ }, []);
+
+ const login = async (username: string, password: string): Promise<{ success: boolean; message: string }> => {
+ try {
+ const response = await fetch(SSO_API_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ username,
+ password,
+ api_key: SSO_API_KEY,
+ }),
+ });
+
+ const data = await response.json();
+
+ if (data.authorized && data.user) {
+ setUser(data.user);
+ localStorage.setItem("auth_user", JSON.stringify(data.user));
+ return { success: true, message: data.message };
+ } else {
+ return { success: false, message: data.message || "Authentication failed" };
+ }
+ } catch (error) {
+ console.error("Login error:", error);
+ return { success: false, message: "Connection error. Please try again." };
+ }
+ };
+
+ const logout = () => {
+ setUser(null);
+ localStorage.removeItem("auth_user");
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error("useAuth must be used within an AuthProvider");
+ }
+ return context;
+}
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..0a2a24c
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,119 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useAuth } from "@/contexts/AuthContext";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Loader2, LogIn, AlertCircle } from "lucide-react";
+
+export default function LoginPage() {
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError("");
+ setIsLoading(true);
+
+ const result = await login(username, password);
+
+ if (result.success) {
+ navigate("/", { replace: true });
+ } else {
+ setError(result.message);
+ }
+
+ setIsLoading(false);
+ };
+
+ return (
+
+
+ {/* Logo */}
+
+

+
+
Traceability Dashboard
+
ASF Sensor Hub
+
+
+
+ {/* Login Card */}
+
+
+ Sign in
+
+ Enter your credentials to access the dashboard
+
+
+
+
+
+
+
+
+ Contact your administrator if you need access
+
+
+
+ );
+}