RBAC seems to be working
This commit is contained in:
135
src/App.tsx
135
src/App.tsx
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
209
src/components/Dashboard.tsx
Normal file
209
src/components/Dashboard.tsx
Normal 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
110
src/components/Login.tsx
Normal 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
23
src/lib/supabase.ts
Normal 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
9
src/vite-env.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user