initial commit
This commit is contained in:
30
frontend/src/App.tsx
Normal file
30
frontend/src/App.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import Layout from '@/components/Layout'
|
||||
import HomePage from '@/pages/HomePage'
|
||||
import DashboardPage from '@/pages/DashboardPage'
|
||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Layout>
|
||||
<HomePage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
27
frontend/src/components/Layout.tsx
Normal file
27
frontend/src/components/Layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import Navbar from './Navbar'
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Navbar />
|
||||
<main className="flex-1">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<footer className="border-t border-gray-200 bg-white py-6">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
© {new Date().getFullYear()} Keycloak Auth App. All rights
|
||||
reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
72
frontend/src/components/Navbar.tsx
Normal file
72
frontend/src/components/Navbar.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useAuth } from '@/hooks'
|
||||
|
||||
export default function Navbar() {
|
||||
const { user, isAuthenticated, login, logout } = useAuth()
|
||||
|
||||
return (
|
||||
<nav className="border-b border-gray-200 bg-white shadow-sm">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
{/* Logo / Brand */}
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-xl font-bold text-primary-600 hover:text-primary-700"
|
||||
>
|
||||
<svg
|
||||
className="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Auth App</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="flex items-center gap-4">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-600">
|
||||
Hello,{' '}
|
||||
<span className="font-medium text-gray-900">
|
||||
{user?.full_name || user?.preferred_username}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="rounded-md bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={login}
|
||||
className="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-700"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
28
frontend/src/components/ProtectedRoute.tsx
Normal file
28
frontend/src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { useAuth } from '@/hooks'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const { isAuthenticated, isLoading } = useAuth()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-4 border-primary-200 border-t-primary-600" />
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
3
frontend/src/components/index.ts
Normal file
3
frontend/src/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Layout } from './Layout'
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as ProtectedRoute } from './ProtectedRoute'
|
||||
64
frontend/src/context/AuthContext.tsx
Normal file
64
frontend/src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
createContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import type { User, AuthContextType } from '@/types'
|
||||
import { authService } from '@/services'
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const refreshUser = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const currentUser = await authService.getCurrentUser()
|
||||
setUser(currentUser)
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh user:', error)
|
||||
setUser(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const login = useCallback(() => {
|
||||
authService.login()
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await authService.logout()
|
||||
setUser(null)
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error)
|
||||
// Still clear user on frontend even if backend logout fails
|
||||
setUser(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Check authentication status on mount
|
||||
useEffect(() => {
|
||||
refreshUser()
|
||||
}, [refreshUser])
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated: !!user,
|
||||
login,
|
||||
logout,
|
||||
refreshUser,
|
||||
}
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
1
frontend/src/context/index.ts
Normal file
1
frontend/src/context/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AuthContext, AuthProvider } from './AuthContext'
|
||||
1
frontend/src/hooks/index.ts
Normal file
1
frontend/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useAuth } from './useAuth'
|
||||
13
frontend/src/hooks/useAuth.ts
Normal file
13
frontend/src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useContext } from 'react'
|
||||
import { AuthContext } from '@/context'
|
||||
import type { AuthContextType } from '@/types'
|
||||
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
7
frontend/src/index.css
Normal file
7
frontend/src/index.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-900 antialiased;
|
||||
}
|
||||
16
frontend/src/main.tsx
Normal file
16
frontend/src/main.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App'
|
||||
import { AuthProvider } from '@/context/AuthContext'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
)
|
||||
245
frontend/src/pages/DashboardPage.tsx
Normal file
245
frontend/src/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { useAuth } from '@/hooks'
|
||||
|
||||
// Icons as components for cleaner code
|
||||
const DataServicesIcon = () => (
|
||||
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="12" y="8" width="40" height="48" rx="2" />
|
||||
<line x1="20" y1="20" x2="44" y2="20" />
|
||||
<line x1="20" y1="28" x2="44" y2="28" />
|
||||
<line x1="20" y1="36" x2="44" y2="36" />
|
||||
<line x1="20" y1="44" x2="36" y2="44" />
|
||||
<rect x="36" y="40" width="12" height="12" rx="1" />
|
||||
<line x1="40" y1="44" x2="44" y2="44" />
|
||||
<line x1="40" y1="48" x2="44" y2="48" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const IntegrationIcon = () => (
|
||||
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="8" y="16" width="20" height="14" rx="2" />
|
||||
<rect x="36" y="16" width="20" height="14" rx="2" />
|
||||
<rect x="22" y="38" width="20" height="14" rx="2" />
|
||||
<line x1="18" y1="30" x2="18" y2="38" />
|
||||
<line x1="18" y1="38" x2="32" y2="38" />
|
||||
<line x1="46" y1="30" x2="46" y2="38" />
|
||||
<line x1="46" y1="38" x2="32" y2="38" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const IntelligenceIcon = () => (
|
||||
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="28" cy="28" r="14" />
|
||||
<line x1="38" y1="38" x2="52" y2="52" strokeWidth="4" strokeLinecap="round" />
|
||||
<path d="M22 28 L26 32 L34 24" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const UserExperienceIcon = () => (
|
||||
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="32" cy="20" r="8" />
|
||||
<circle cx="16" cy="36" r="4" />
|
||||
<circle cx="48" cy="36" r="4" />
|
||||
<circle cx="24" cy="52" r="4" />
|
||||
<circle cx="40" cy="52" r="4" />
|
||||
<line x1="32" y1="28" x2="32" y2="36" />
|
||||
<line x1="32" y1="36" x2="16" y2="36" />
|
||||
<line x1="32" y1="36" x2="48" y2="36" />
|
||||
<line x1="20" y1="40" x2="24" y2="48" />
|
||||
<line x1="44" y1="40" x2="40" y2="48" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ManagementIcon = () => (
|
||||
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="32" cy="32" r="12" />
|
||||
<path d="M32 16 L32 20" />
|
||||
<path d="M32 44 L32 48" />
|
||||
<path d="M16 32 L20 32" />
|
||||
<path d="M44 32 L48 32" />
|
||||
<path d="M20.7 20.7 L23.5 23.5" />
|
||||
<path d="M40.5 40.5 L43.3 43.3" />
|
||||
<path d="M20.7 43.3 L23.5 40.5" />
|
||||
<path d="M40.5 23.5 L43.3 20.7" />
|
||||
<rect x="26" y="28" width="12" height="10" rx="1" />
|
||||
<path d="M29 28 L29 26 L35 26 L35 28" />
|
||||
<line x1="29" y1="32" x2="35" y2="32" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const TrustworthinessIcon = () => (
|
||||
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 36 C12 36 20 28 32 36 C44 44 52 36 52 36" strokeLinecap="round" />
|
||||
<path d="M12 36 L24 48 L32 36" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M52 36 L40 48 L32 36" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Header */}
|
||||
<header className="py-6 text-center">
|
||||
<h1 className="text-3xl font-semibold text-teal-700">
|
||||
Digital Twin Requirements Tool
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{/* Top Bar */}
|
||||
<div className="border-y border-gray-200 py-3 px-8">
|
||||
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600">Projects</span>
|
||||
<span className="mx-2 text-gray-400">»</span>
|
||||
<span className="font-semibold text-gray-900">PeTWIN</span>
|
||||
</div>
|
||||
|
||||
{/* Language Toggle */}
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<span>English</span>
|
||||
<div className="relative inline-flex h-5 w-10 items-center rounded-full bg-gray-300">
|
||||
<span className="inline-block h-4 w-4 transform rounded-full bg-white shadow-sm translate-x-0.5" />
|
||||
</div>
|
||||
<span>Portuguese</span>
|
||||
</div>
|
||||
|
||||
{/* Admin Panel Button */}
|
||||
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
Admin Panel
|
||||
</button>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-700">
|
||||
{user?.full_name || user?.preferred_username || 'User'}{' '}
|
||||
<span className="text-gray-500">(admin)</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-7xl mx-auto px-8 py-8">
|
||||
<div className="flex gap-12">
|
||||
{/* Left Sidebar */}
|
||||
<div className="w-64 flex-shrink-0">
|
||||
{/* Requirements Search */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-1">Requirements Search</h2>
|
||||
<p className="text-sm text-teal-600">
|
||||
Search for specific elements by name or symbol.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Create a Requirement */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-1">Create a Requirement</h2>
|
||||
<p className="text-sm text-teal-600">
|
||||
Register a new Requirement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Last Viewed Requirement */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-1">Last Viewed Requirement:</h2>
|
||||
<p className="text-sm text-gray-600 mb-1">No requirement accessed yet</p>
|
||||
<a href="#" className="text-sm text-teal-600 hover:underline">
|
||||
View More
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* My Requirements */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-1">My Requirements</h2>
|
||||
<p className="text-sm text-teal-600">
|
||||
View your requirements and their properties.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Requirement Report */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-1">Requirement Report</h2>
|
||||
<p className="text-sm text-teal-600">
|
||||
Generate the current status of this projects requirements in PDF format
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Content - Quick Search Filters */}
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-gray-700 mb-6 text-center">
|
||||
Quick Search Filters
|
||||
</h2>
|
||||
|
||||
{/* Grid Layout matching the screenshot */}
|
||||
<div className="grid grid-cols-4 gap-0 max-w-2xl mx-auto">
|
||||
{/* Row 1 */}
|
||||
{/* Data Services - spans 2 rows */}
|
||||
<div className="row-span-2 bg-blue-200 border border-blue-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-blue-300 transition-colors min-h-[200px]">
|
||||
<div className="text-blue-800">
|
||||
<DataServicesIcon />
|
||||
</div>
|
||||
<span className="mt-3 text-sm font-semibold text-blue-900">Data Services</span>
|
||||
</div>
|
||||
|
||||
{/* Integration - 1 row */}
|
||||
<div className="bg-amber-200 border border-amber-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-amber-300 transition-colors min-h-[100px]">
|
||||
<div className="text-amber-800">
|
||||
<IntegrationIcon />
|
||||
</div>
|
||||
<span className="mt-2 text-sm font-semibold text-amber-900">Integration</span>
|
||||
</div>
|
||||
|
||||
{/* Intelligence - spans 2 rows */}
|
||||
<div className="row-span-2 bg-purple-200 border border-purple-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-purple-300 transition-colors min-h-[200px]">
|
||||
<div className="text-purple-800">
|
||||
<IntelligenceIcon />
|
||||
</div>
|
||||
<span className="mt-3 text-sm font-semibold text-purple-900">Intelligence</span>
|
||||
</div>
|
||||
|
||||
{/* User Experience - spans 2 rows */}
|
||||
<div className="row-span-2 bg-green-200 border border-green-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-green-300 transition-colors min-h-[200px]">
|
||||
<div className="text-green-800">
|
||||
<UserExperienceIcon />
|
||||
</div>
|
||||
<span className="mt-3 text-sm font-semibold text-green-900">User Experience</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2 - Management and Trustworthiness */}
|
||||
{/* Management - 1 row, spans 1 col */}
|
||||
<div className="bg-red-200 border border-red-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-red-300 transition-colors min-h-[100px]">
|
||||
<div className="text-red-800">
|
||||
<ManagementIcon />
|
||||
</div>
|
||||
<span className="mt-2 text-sm font-semibold text-red-900">Management</span>
|
||||
</div>
|
||||
|
||||
{/* Trustworthiness - 1 row */}
|
||||
<div className="bg-green-100 border border-green-300 flex flex-col items-center justify-center p-4 cursor-pointer hover:bg-green-200 transition-colors min-h-[100px]">
|
||||
<div className="text-red-400">
|
||||
<TrustworthinessIcon />
|
||||
</div>
|
||||
<span className="mt-2 text-sm font-semibold text-red-600">Trustworthiness</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
151
frontend/src/pages/HomePage.tsx
Normal file
151
frontend/src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useAuth } from '@/hooks'
|
||||
|
||||
export default function HomePage() {
|
||||
const { isAuthenticated, login, isLoading } = useAuth()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-4 border-primary-200 border-t-primary-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[60vh] flex-col items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl md:text-6xl">
|
||||
Welcome to{' '}
|
||||
<span className="text-primary-600">Keycloak Auth</span>
|
||||
</h1>
|
||||
<p className="mx-auto mt-6 max-w-2xl text-lg text-gray-600">
|
||||
A secure authentication system powered by Keycloak and FastAPI. Login
|
||||
with your Keycloak credentials to access protected resources.
|
||||
</p>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<div className="mt-10">
|
||||
<button
|
||||
onClick={login}
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-8 py-4 text-lg font-semibold text-white shadow-lg transition-all hover:bg-primary-700 hover:shadow-xl focus:outline-none focus:ring-4 focus:ring-primary-300"
|
||||
>
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Login with Keycloak
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAuthenticated && (
|
||||
<div className="mt-10">
|
||||
<a
|
||||
href="/dashboard"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-8 py-4 text-lg font-semibold text-white shadow-lg transition-all hover:bg-primary-700 hover:shadow-xl focus:outline-none focus:ring-4 focus:ring-primary-300"
|
||||
>
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
|
||||
/>
|
||||
</svg>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<div className="mt-20 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-primary-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-gray-900">
|
||||
Secure Authentication
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
HTTP-only cookies protect your tokens from XSS attacks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-primary-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-gray-900">
|
||||
Keycloak Integration
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Enterprise-grade identity management with Keycloak SSO.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-primary-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-gray-900">
|
||||
FastAPI Backend
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
High-performance Python backend with automatic API documentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
2
frontend/src/pages/index.ts
Normal file
2
frontend/src/pages/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as HomePage } from './HomePage'
|
||||
export { default as DashboardPage } from './DashboardPage'
|
||||
67
frontend/src/services/authService.ts
Normal file
67
frontend/src/services/authService.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { User } from '@/types'
|
||||
|
||||
const API_BASE_URL = '/api'
|
||||
|
||||
class AuthService {
|
||||
/**
|
||||
* Get the current authenticated user from the session cookie.
|
||||
* Returns null if not authenticated.
|
||||
*/
|
||||
async getCurrentUser(): Promise<User | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/me`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
return null
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const user: User = await response.json()
|
||||
return user
|
||||
} catch (error) {
|
||||
console.error('Failed to get current user:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the current user by clearing the session cookie.
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the login endpoint to initiate OAuth flow.
|
||||
*/
|
||||
login(): void {
|
||||
// In development with Vite proxy, we can use relative URL
|
||||
// In production, this will be proxied by nginx
|
||||
window.location.href = `${API_BASE_URL}/login`
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService()
|
||||
1
frontend/src/services/index.ts
Normal file
1
frontend/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { authService } from './authService'
|
||||
14
frontend/src/types/auth.ts
Normal file
14
frontend/src/types/auth.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface User {
|
||||
preferred_username: string
|
||||
email: string | null
|
||||
full_name: string | null
|
||||
}
|
||||
|
||||
export interface AuthContextType {
|
||||
user: User | null
|
||||
isLoading: boolean
|
||||
isAuthenticated: boolean
|
||||
login: () => void
|
||||
logout: () => Promise<void>
|
||||
refreshUser: () => Promise<void>
|
||||
}
|
||||
1
frontend/src/types/index.ts
Normal file
1
frontend/src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth'
|
||||
6
frontend/src/vite-env.d.ts
vendored
Normal file
6
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.css' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
Reference in New Issue
Block a user