RBAC seems to be working

This commit is contained in:
Alireza Vaezi
2025-07-20 11:05:58 -04:00
parent 033229989a
commit 6a9ab6afaa
12 changed files with 987 additions and 25 deletions

View File

@@ -1,33 +1,120 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import { useState, useEffect } from 'react'
import { supabase } from './lib/supabase'
import { Login } from './components/Login'
import { Dashboard } from './components/Dashboard'
import './App.css'
function App() {
const [count, setCount] = useState(0)
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null)
const [loading, setLoading] = useState(true)
const [currentRoute, setCurrentRoute] = useState(window.location.pathname)
useEffect(() => {
// Check initial auth state
checkAuthState()
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth state changed:', event, !!session)
setIsAuthenticated(!!session)
setLoading(false)
// Handle signout route
if (event === 'SIGNED_OUT') {
setCurrentRoute('/')
window.history.pushState({}, '', '/')
}
})
// Handle browser navigation
const handlePopState = () => {
setCurrentRoute(window.location.pathname)
}
window.addEventListener('popstate', handlePopState)
return () => {
subscription.unsubscribe()
window.removeEventListener('popstate', handlePopState)
}
}, [])
useEffect(() => {
// Handle signout route
if (currentRoute === '/signout') {
handleLogout()
}
}, [currentRoute])
const checkAuthState = async () => {
try {
const { data: { session } } = await supabase.auth.getSession()
setIsAuthenticated(!!session)
} catch (error) {
console.error('Error checking auth state:', error)
setIsAuthenticated(false)
} finally {
setLoading(false)
}
}
const handleLoginSuccess = () => {
setIsAuthenticated(true)
setCurrentRoute('/')
window.history.pushState({}, '', '/')
}
const handleLogout = async () => {
try {
// Clear Supabase session
await supabase.auth.signOut()
// Clear any local storage items
localStorage.removeItem('supabase.auth.token')
// Reset state
setIsAuthenticated(false)
setCurrentRoute('/')
window.history.pushState({}, '', '/')
} catch (error) {
console.error('Logout error:', error)
// Still reset state even if there's an error
setIsAuthenticated(false)
setCurrentRoute('/')
window.history.pushState({}, '', '/')
}
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
)
}
// Handle signout route
if (currentRoute === '/signout') {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Signing out...</p>
</div>
</div>
)
}
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
{isAuthenticated ? (
<Dashboard onLogout={handleLogout} />
) : (
<Login onLoginSuccess={handleLoginSuccess} />
)}
</>
)
}

View File

@@ -0,0 +1,209 @@
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'
import type { User } from '../lib/supabase'
interface DashboardProps {
onLogout: () => void
}
export function Dashboard({ onLogout }: DashboardProps) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetchUserProfile()
}, [])
const fetchUserProfile = async () => {
try {
setLoading(true)
setError(null)
// Get current auth user
const { data: { user: authUser }, error: authError } = await supabase.auth.getUser()
if (authError) {
setError('Failed to get authenticated user')
return
}
if (!authUser) {
setError('No authenticated user found')
return
}
// Get user profile with role information
const { data: profile, error: profileError } = await supabase
.from('user_profiles')
.select(`
id,
email,
created_at,
updated_at,
role_id,
roles!inner (
name,
description
)
`)
.eq('id', authUser.id)
.single()
if (profileError) {
setError('Failed to fetch user profile: ' + profileError.message)
return
}
if (profile) {
setUser({
id: profile.id,
email: profile.email,
role: profile.roles.name as 'admin' | 'conductor' | 'analyst',
created_at: profile.created_at,
updated_at: profile.updated_at
})
}
} catch (err) {
setError('An unexpected error occurred')
console.error('Profile fetch error:', err)
} finally {
setLoading(false)
}
}
const handleLogout = async () => {
// Navigate to signout route which will handle the actual logout
window.history.pushState({}, '', '/signout')
window.dispatchEvent(new PopStateEvent('popstate'))
}
const handleDirectLogout = async () => {
try {
const { error } = await supabase.auth.signOut()
if (error) {
console.error('Logout error:', error)
}
onLogout()
} catch (err) {
console.error('Logout error:', err)
onLogout() // Still call onLogout to reset the UI state
}
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading user profile...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full">
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
</div>
<button
onClick={handleLogout}
className="mt-4 w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-gray-600 hover:bg-gray-700"
>
Back to Login
</button>
</div>
</div>
)
}
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'admin':
return 'bg-red-100 text-red-800'
case 'conductor':
return 'bg-blue-100 text-blue-800'
case 'analyst':
return 'bg-green-100 text-green-800'
default:
return 'bg-gray-100 text-gray-800'
}
}
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="border-4 border-dashed border-gray-200 rounded-lg p-8">
<div className="flex justify-between items-start mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="mt-2 text-gray-600">Welcome to the RBAC system</p>
</div>
<div className="flex gap-2">
<button
onClick={handleLogout}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Sign Out (/signout)
</button>
<button
onClick={handleDirectLogout}
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Direct Logout
</button>
</div>
</div>
{user && (
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
User Information
</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
Your account details and role permissions.
</p>
</div>
<div className="border-t border-gray-200">
<dl>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">Email</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{user.email}
</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">Role</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getRoleBadgeColor(user.role)}`}>
{user.role.charAt(0).toUpperCase() + user.role.slice(1)}
</span>
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">User ID</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2 font-mono">
{user.id}
</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500">Member since</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{new Date(user.created_at).toLocaleDateString()}
</dd>
</div>
</dl>
</div>
</div>
)}
</div>
</div>
</div>
</div>
)
}

110
src/components/Login.tsx Normal file
View File

@@ -0,0 +1,110 @@
import { useState } from 'react'
import { supabase } from '../lib/supabase'
interface LoginProps {
onLoginSuccess: () => void
}
export function Login({ onLoginSuccess }: LoginProps) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)
try {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
setError(error.message)
} else if (data.user) {
onLoginSuccess()
}
} catch (err) {
setError('An unexpected error occurred')
console.error('Login error:', err)
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
RBAC Authentication System
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
</div>
)}
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</div>
<div className="text-center">
<p className="text-sm text-gray-600">
Test credentials: s.alireza.v@gmail.com / 2517392
</p>
</div>
</form>
</div>
</div>
)
}

23
src/lib/supabase.ts Normal file
View File

@@ -0,0 +1,23 @@
import { createClient } from '@supabase/supabase-js'
// Local development configuration
const supabaseUrl = 'http://127.0.0.1:54321'
const supabaseAnonKey = '[REDACTED]'
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// Database types for TypeScript
export interface User {
id: string
email: string
role: 'admin' | 'conductor' | 'analyst'
created_at: string
updated_at: string
}
export interface Role {
id: string
name: 'admin' | 'conductor' | 'analyst'
description: string
created_at: string
}

9
src/vite-env.d.ts vendored
View File

@@ -1 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_SUPABASE_URL: string;
readonly VITE_SUPABASE_ANON_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}