From 53b5bbd3a6a914cfaf3c249d8d5fdf839a9b8b9f Mon Sep 17 00:00:00 2001 From: mahmamdouh Date: Sun, 25 Jan 2026 17:49:55 +0100 Subject: [PATCH] sso --- docker-compose.yml | 4 +- src/App.tsx | 68 +++++++++++++--- src/components/ProtectedRoute.tsx | 29 +++++++ src/components/layout/AppLayout.tsx | 46 ++++++++++- src/contexts/AuthContext.tsx | 102 ++++++++++++++++++++++++ src/pages/LoginPage.tsx | 119 ++++++++++++++++++++++++++++ 6 files changed, 355 insertions(+), 13 deletions(-) create mode 100644 src/components/ProtectedRoute.tsx create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/pages/LoginPage.tsx 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 ( +
+
+ +

Loading...

+
+
+ ); + } + + 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 */} +
+ NABD Solutions +
+

Traceability Dashboard

+

ASF Sensor Hub

+
+
+ + {/* Login Card */} + + + Sign in + + Enter your credentials to access the dashboard + + + +
+ {error && ( + + + {error} + + )} + +
+ + setUsername(e.target.value)} + disabled={isLoading} + required + autoComplete="username" + /> +
+ +
+ + setPassword(e.target.value)} + disabled={isLoading} + required + autoComplete="current-password" + /> +
+ + +
+
+
+ +

+ Contact your administrator if you need access +

+
+
+ ); +}