Revert "RBAC in place. Tailwind CSS working."
This reverts commit 90d874b15f.
This commit is contained in:
148
src/App.tsx
148
src/App.tsx
@@ -1,126 +1,34 @@
|
||||
import React from 'react'
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||
import { LoginForm } from './components/auth/LoginForm'
|
||||
import { UserProfile } from './components/auth/UserProfile'
|
||||
import { ProtectedRoute, AdminOnly, ModeratorOrAdmin, AuthenticatedOnly } from './components/auth/ProtectedRoute'
|
||||
|
||||
const AppContent: React.FC = () => {
|
||||
const { user, loading } = useAuth()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-xl">Loading...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<h1 className="text-3xl font-bold text-center mb-8">RBAC Demo Application</h1>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<h1 className="text-3xl font-bold text-center mb-8">RBAC Demo Application</h1>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* User Profile Section */}
|
||||
<div>
|
||||
<UserProfile />
|
||||
</div>
|
||||
|
||||
{/* Role-Based Content Section */}
|
||||
<div className="space-y-6">
|
||||
{/* Content for all authenticated users */}
|
||||
<AuthenticatedOnly>
|
||||
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
|
||||
<h3 className="text-lg font-semibold text-green-800 mb-2">
|
||||
✅ Authenticated User Content
|
||||
</h3>
|
||||
<p className="text-green-700">
|
||||
This content is visible to all authenticated users.
|
||||
</p>
|
||||
</div>
|
||||
</AuthenticatedOnly>
|
||||
|
||||
{/* Content for moderators and admins */}
|
||||
<ModeratorOrAdmin>
|
||||
<div className="bg-yellow-50 p-4 rounded-lg border border-yellow-200">
|
||||
<h3 className="text-lg font-semibold text-yellow-800 mb-2">
|
||||
🛡️ Moderator/Admin Content
|
||||
</h3>
|
||||
<p className="text-yellow-700">
|
||||
This content is visible to moderators and administrators only.
|
||||
</p>
|
||||
</div>
|
||||
</ModeratorOrAdmin>
|
||||
|
||||
{/* Content for admins only */}
|
||||
<AdminOnly>
|
||||
<div className="bg-red-50 p-4 rounded-lg border border-red-200">
|
||||
<h3 className="text-lg font-semibold text-red-800 mb-2">
|
||||
🔑 Admin Only Content
|
||||
</h3>
|
||||
<p className="text-red-700">
|
||||
This content is visible to administrators only. You have full system access.
|
||||
</p>
|
||||
</div>
|
||||
</AdminOnly>
|
||||
|
||||
{/* Custom role check example */}
|
||||
<ProtectedRoute
|
||||
requiredRole="user"
|
||||
fallback={
|
||||
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
||||
<p className="text-gray-600">You need 'user' role to see this content.</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<h3 className="text-lg font-semibold text-blue-800 mb-2">
|
||||
👤 User Role Content
|
||||
</h3>
|
||||
<p className="text-blue-700">
|
||||
This content is visible to users with the 'user' role.
|
||||
</p>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="mt-8 bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-bold mb-4">RBAC System Instructions</h2>
|
||||
<div className="space-y-2 text-sm text-gray-600">
|
||||
<p><strong>Admin User:</strong> s.alireza.v@gmail.com (password: ???????)</p>
|
||||
<p><strong>Features:</strong></p>
|
||||
<ul className="list-disc list-inside ml-4 space-y-1">
|
||||
<li>Role-based content visibility</li>
|
||||
<li>Protected routes and components</li>
|
||||
<li>User profile with role display</li>
|
||||
<li>Secure authentication with Supabase</li>
|
||||
<li>Row Level Security (RLS) policies</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
|
||||
export const LoginForm: React.FC = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const { signIn } = useAuth()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const { error } = await signIn(email, password)
|
||||
if (error) {
|
||||
setError(error.message)
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center">Sign In</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-sm text-gray-600">
|
||||
<p>Test admin credentials:</p>
|
||||
<p>Email: s.alireza.v@gmail.com</p>
|
||||
<p>Password: ???????</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import React from 'react'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import type { RoleName } from '../../types/auth'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode
|
||||
requiredRole?: RoleName
|
||||
requiredRoles?: RoleName[]
|
||||
fallback?: React.ReactNode
|
||||
requireAuth?: boolean
|
||||
}
|
||||
|
||||
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
children,
|
||||
requiredRole,
|
||||
requiredRoles,
|
||||
fallback = <div className="text-red-500">Access denied. You don't have permission to view this content.</div>,
|
||||
requireAuth = true
|
||||
}) => {
|
||||
const { user, loading } = useAuth()
|
||||
|
||||
// Show loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-gray-500">Loading...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Check if authentication is required
|
||||
if (requireAuth && !user) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-red-500">Please sign in to access this content.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Check single required role
|
||||
if (requiredRole && user && !user.roles?.includes(requiredRole)) {
|
||||
return <>{fallback}</>
|
||||
}
|
||||
|
||||
// Check multiple required roles (user must have at least one)
|
||||
if (requiredRoles && user && !requiredRoles.some(role => user.roles?.includes(role))) {
|
||||
return <>{fallback}</>
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
// Convenience components for common role checks
|
||||
export const AdminOnly: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode }> = ({
|
||||
children,
|
||||
fallback
|
||||
}) => (
|
||||
<ProtectedRoute requiredRole="admin" fallback={fallback}>
|
||||
{children}
|
||||
</ProtectedRoute>
|
||||
)
|
||||
|
||||
export const ModeratorOrAdmin: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode }> = ({
|
||||
children,
|
||||
fallback
|
||||
}) => (
|
||||
<ProtectedRoute requiredRoles={['admin', 'moderator']} fallback={fallback}>
|
||||
{children}
|
||||
</ProtectedRoute>
|
||||
)
|
||||
|
||||
export const AuthenticatedOnly: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode }> = ({
|
||||
children,
|
||||
fallback
|
||||
}) => (
|
||||
<ProtectedRoute requireAuth={true} fallback={fallback}>
|
||||
{children}
|
||||
</ProtectedRoute>
|
||||
)
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
|
||||
export const UserProfile: React.FC = () => {
|
||||
const { user, signOut, isAdmin } = useAuth()
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-bold mb-4">User Profile</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Email</label>
|
||||
<p className="text-gray-900">{user.email}</p>
|
||||
</div>
|
||||
|
||||
{user.profile && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">First Name</label>
|
||||
<p className="text-gray-900">{user.profile.first_name || 'Not set'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Last Name</label>
|
||||
<p className="text-gray-900">{user.profile.last_name || 'Not set'}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Roles</label>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{user.roles && user.roles.length > 0 ? (
|
||||
user.roles.map((role) => (
|
||||
<span
|
||||
key={role}
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
role === 'admin'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: role === 'moderator'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-blue-100 text-blue-800'
|
||||
}`}
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-gray-500">No roles assigned</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAdmin() && (
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
||||
<p className="text-sm text-red-700 font-medium">
|
||||
🔑 You have administrator privileges
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={signOut}
|
||||
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
import type { User } from '@supabase/supabase-js'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import type { AuthContextType, AuthUser, UserProfile } from '../types/auth'
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const [user, setUser] = useState<AuthUser | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Fetch user profile and roles
|
||||
const fetchUserData = async (authUser: User): Promise<AuthUser> => {
|
||||
try {
|
||||
// Fetch user profile
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('*')
|
||||
.eq('id', authUser.id)
|
||||
.single()
|
||||
|
||||
if (profileError && profileError.code !== 'PGRST116') {
|
||||
console.error('Error fetching user profile:', profileError)
|
||||
}
|
||||
|
||||
// Fetch user roles using the database function
|
||||
const { data: rolesData, error: rolesError } = await supabase
|
||||
.rpc('get_user_roles', { user_uuid: authUser.id })
|
||||
|
||||
if (rolesError) {
|
||||
console.error('Error fetching user roles:', rolesError)
|
||||
}
|
||||
|
||||
const roles = rolesData?.map(r => r.role_name) || []
|
||||
|
||||
return {
|
||||
...authUser,
|
||||
profile: profile as UserProfile,
|
||||
roles
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error)
|
||||
return {
|
||||
...authUser,
|
||||
profile: undefined,
|
||||
roles: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh user data
|
||||
const refreshUserData = async () => {
|
||||
const { data: { user: authUser } } = await supabase.auth.getUser()
|
||||
if (authUser) {
|
||||
const userData = await fetchUserData(authUser)
|
||||
setUser(userData)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign in
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
})
|
||||
return { error }
|
||||
}
|
||||
|
||||
// Sign up
|
||||
const signUp = async (
|
||||
email: string,
|
||||
password: string,
|
||||
userData?: { first_name?: string; last_name?: string }
|
||||
) => {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: userData
|
||||
}
|
||||
})
|
||||
|
||||
// If signup successful and user data provided, create profile
|
||||
if (!error && data.user && userData) {
|
||||
const { error: profileError } = await supabase
|
||||
.from('user_profiles')
|
||||
.insert({
|
||||
id: data.user.id,
|
||||
first_name: userData.first_name,
|
||||
last_name: userData.last_name
|
||||
})
|
||||
|
||||
if (profileError) {
|
||||
console.error('Error creating user profile:', profileError)
|
||||
}
|
||||
|
||||
// Assign default 'user' role
|
||||
const { error: roleError } = await supabase
|
||||
.from('user_roles')
|
||||
.insert({
|
||||
user_id: data.user.id,
|
||||
role_id: 6 // 'user' role ID from our database
|
||||
})
|
||||
|
||||
if (roleError) {
|
||||
console.error('Error assigning default role:', roleError)
|
||||
}
|
||||
}
|
||||
|
||||
return { error }
|
||||
}
|
||||
|
||||
// Sign out
|
||||
const signOut = async () => {
|
||||
await supabase.auth.signOut()
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
// Check if user has specific role
|
||||
const hasRole = (roleName: string): boolean => {
|
||||
return user?.roles?.includes(roleName) || false
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
const isAdmin = (): boolean => {
|
||||
return hasRole('admin')
|
||||
}
|
||||
|
||||
// Handle auth state changes
|
||||
useEffect(() => {
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
if (session?.user) {
|
||||
const userData = await fetchUserData(session.user)
|
||||
setUser(userData)
|
||||
} else {
|
||||
setUser(null)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
)
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
loading,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
hasRole,
|
||||
isAdmin,
|
||||
refreshUserData
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1 +1,68 @@
|
||||
@import "tailwindcss";
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// Local Supabase instance configuration
|
||||
const supabaseUrl = 'http://127.0.0.1:54321'
|
||||
const supabaseAnonKey = '[REDACTED]'
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
|
||||
// Database types for TypeScript
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
roles: {
|
||||
Row: {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: number
|
||||
name: string
|
||||
description?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: number
|
||||
name?: string
|
||||
description?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
user_profiles: {
|
||||
Row: {
|
||||
id: string
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id: string
|
||||
first_name?: string | null
|
||||
last_name?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
first_name?: string | null
|
||||
last_name?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
user_roles: {
|
||||
Row: {
|
||||
id: number
|
||||
user_id: string | null
|
||||
role_id: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: number
|
||||
user_id?: string | null
|
||||
role_id?: number | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: number
|
||||
user_id?: string | null
|
||||
role_id?: number | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
get_user_roles: {
|
||||
Args: {
|
||||
user_uuid: string
|
||||
}
|
||||
Returns: {
|
||||
role_name: string
|
||||
}[]
|
||||
}
|
||||
user_has_role: {
|
||||
Args: {
|
||||
user_uuid: string
|
||||
role_name: string
|
||||
}
|
||||
Returns: boolean
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { User } from '@supabase/supabase-js'
|
||||
|
||||
export interface UserProfile {
|
||||
id: string
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface UserRole {
|
||||
id: number
|
||||
user_id: string | null
|
||||
role_id: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AuthUser extends User {
|
||||
profile?: UserProfile
|
||||
roles?: string[]
|
||||
}
|
||||
|
||||
export interface AuthContextType {
|
||||
user: AuthUser | null
|
||||
loading: boolean
|
||||
signIn: (email: string, password: string) => Promise<{ error: any }>
|
||||
signUp: (email: string, password: string, userData?: { first_name?: string; last_name?: string }) => Promise<{ error: any }>
|
||||
signOut: () => Promise<void>
|
||||
hasRole: (roleName: string) => boolean
|
||||
isAdmin: () => boolean
|
||||
refreshUserData: () => Promise<void>
|
||||
}
|
||||
|
||||
export type RoleName = 'admin' | 'user' | 'moderator' | 'coordinator' | 'conductor' | 'analyst'
|
||||
8
supabase/.gitignore
vendored
8
supabase/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
|
||||
# dotenvx
|
||||
.env.keys
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -1,322 +0,0 @@
|
||||
# For detailed configuration reference documentation, visit:
|
||||
# https://supabase.com/docs/guides/local-development/cli/config
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "pecan_experiments"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. `public` and `graphql_public` schemas are included by default.
|
||||
schemas = ["public", "graphql_public"]
|
||||
# Extra schemas to add to the search_path of every request.
|
||||
extra_search_path = ["public", "extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[api.tls]
|
||||
# Enable HTTPS endpoints locally using a self-signed certificate.
|
||||
enabled = false
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 17
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
# How many server connections to allow per user/database pair.
|
||||
default_pool_size = 20
|
||||
# Maximum number of client connections allowed.
|
||||
max_client_conn = 100
|
||||
|
||||
# [db.vault]
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[db.migrations]
|
||||
# If disabled, migrations will be skipped during a db push or reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of schema files that describe your database.
|
||||
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
|
||||
schema_paths = []
|
||||
|
||||
[db.seed]
|
||||
# If enabled, seeds the database after migrations during a db reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of seed files to load during db reset.
|
||||
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
||||
sql_paths = ["./seed.sql"]
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||
# ip_version = "IPv6"
|
||||
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||
# max_header_length = 4096
|
||||
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
# External URL of the API server that frontend connects to.
|
||||
api_url = "http://127.0.0.1"
|
||||
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||
openai_api_key = "env(OPENAI_API_KEY)"
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
[storage]
|
||||
enabled = true
|
||||
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||
file_size_limit = "50MiB"
|
||||
|
||||
# Image transformation API is available to Supabase Pro plan.
|
||||
# [storage.image_transformation]
|
||||
# enabled = true
|
||||
|
||||
# Uncomment to configure local storage buckets
|
||||
# [storage.buckets.images]
|
||||
# public = false
|
||||
# file_size_limit = "50MiB"
|
||||
# allowed_mime_types = ["image/png", "image/jpeg"]
|
||||
# objects_path = "./images"
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://127.0.0.1:3000"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
# Requires enable_refresh_token_rotation = true.
|
||||
refresh_token_reuse_interval = 10
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
# Allow/disallow anonymous sign-ins to your project.
|
||||
enable_anonymous_sign_ins = false
|
||||
# Allow/disallow testing manual linking of accounts
|
||||
enable_manual_linking = false
|
||||
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
|
||||
minimum_password_length = 6
|
||||
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
|
||||
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
|
||||
password_requirements = ""
|
||||
|
||||
[auth.rate_limit]
|
||||
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
|
||||
email_sent = 2
|
||||
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
|
||||
sms_sent = 30
|
||||
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
|
||||
anonymous_users = 30
|
||||
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
|
||||
token_refresh = 150
|
||||
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
|
||||
sign_in_sign_ups = 30
|
||||
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
|
||||
token_verifications = 30
|
||||
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
|
||||
web3 = 30
|
||||
|
||||
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
|
||||
# [auth.captcha]
|
||||
# enabled = true
|
||||
# provider = "hcaptcha"
|
||||
# secret = ""
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = false
|
||||
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
|
||||
secure_password_change = false
|
||||
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||
max_frequency = "1s"
|
||||
# Number of characters used in the email OTP.
|
||||
otp_length = 6
|
||||
# Number of seconds before the email OTP expires (defaults to 1 hour).
|
||||
otp_expiry = 3600
|
||||
|
||||
# Use a production-ready SMTP server
|
||||
# [auth.email.smtp]
|
||||
# enabled = true
|
||||
# host = "smtp.sendgrid.net"
|
||||
# port = 587
|
||||
# user = "apikey"
|
||||
# pass = "env(SENDGRID_API_KEY)"
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
# Uncomment to customize email template
|
||||
# [auth.email.template.invite]
|
||||
# subject = "You have been invited"
|
||||
# content_path = "./supabase/templates/invite.html"
|
||||
|
||||
[auth.sms]
|
||||
# Allow/disallow new user signups via SMS to your project.
|
||||
enable_signup = false
|
||||
# If enabled, users need to confirm their phone number before signing in.
|
||||
enable_confirmations = false
|
||||
# Template for sending OTP to users
|
||||
template = "Your code is {{ .Code }}"
|
||||
# Controls the minimum amount of time that must pass before sending another sms otp.
|
||||
max_frequency = "5s"
|
||||
|
||||
# Use pre-defined map of phone number to OTP for testing.
|
||||
# [auth.sms.test_otp]
|
||||
# 4152127777 = "123456"
|
||||
|
||||
# Configure logged in session timeouts.
|
||||
# [auth.sessions]
|
||||
# Force log out after the specified duration.
|
||||
# timebox = "24h"
|
||||
# Force log out if the user has been inactive longer than the specified duration.
|
||||
# inactivity_timeout = "8h"
|
||||
|
||||
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
|
||||
# [auth.hook.before_user_created]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://postgres/auth/before-user-created-hook"
|
||||
|
||||
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||
# [auth.hook.custom_access_token]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://<database>/<schema>/<hook_name>"
|
||||
|
||||
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
||||
[auth.sms.twilio]
|
||||
enabled = false
|
||||
account_sid = ""
|
||||
message_service_sid = ""
|
||||
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||
|
||||
# Multi-factor-authentication is available to Supabase Pro plan.
|
||||
[auth.mfa]
|
||||
# Control how many MFA factors can be enrolled at once per user.
|
||||
max_enrolled_factors = 10
|
||||
|
||||
# Control MFA via App Authenticator (TOTP)
|
||||
[auth.mfa.totp]
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
|
||||
# Configure MFA via Phone Messaging
|
||||
[auth.mfa.phone]
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
otp_length = 6
|
||||
template = "Your code is {{ .Code }}"
|
||||
max_frequency = "5s"
|
||||
|
||||
# Configure MFA via WebAuthn
|
||||
# [auth.mfa.web_authn]
|
||||
# enroll_enabled = true
|
||||
# verify_enabled = true
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
[auth.external.apple]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
||||
# Overrides the default auth redirectUrl.
|
||||
redirect_uri = ""
|
||||
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||
# or any other third-party OIDC providers.
|
||||
url = ""
|
||||
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||
skip_nonce_check = false
|
||||
|
||||
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
|
||||
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
|
||||
[auth.web3.solana]
|
||||
enabled = false
|
||||
|
||||
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.firebase]
|
||||
enabled = false
|
||||
# project_id = "my-firebase-project"
|
||||
|
||||
# Use Auth0 as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.auth0]
|
||||
enabled = false
|
||||
# tenant = "my-auth0-tenant"
|
||||
# tenant_region = "us"
|
||||
|
||||
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.aws_cognito]
|
||||
enabled = false
|
||||
# user_pool_id = "my-user-pool-id"
|
||||
# user_pool_region = "us-east-1"
|
||||
|
||||
# Use Clerk as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.clerk]
|
||||
enabled = false
|
||||
# Obtain from https://clerk.com/setup/supabase
|
||||
# domain = "example.clerk.accounts.dev"
|
||||
|
||||
[edge_runtime]
|
||||
enabled = true
|
||||
# Configure one of the supported request policies: `oneshot`, `per_worker`.
|
||||
# Use `oneshot` for hot reload, or `per_worker` for load testing.
|
||||
policy = "oneshot"
|
||||
# Port to attach the Chrome inspector for debugging edge functions.
|
||||
inspector_port = 8083
|
||||
# The Deno major version to use.
|
||||
deno_version = 1
|
||||
|
||||
# [edge_runtime.secrets]
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[analytics]
|
||||
enabled = true
|
||||
port = 54327
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
|
||||
# Experimental features may be deprecated any time
|
||||
[experimental]
|
||||
# Configures Postgres storage engine to use OrioleDB (S3)
|
||||
orioledb_version = ""
|
||||
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
||||
s3_host = "env(S3_HOST)"
|
||||
# Configures S3 bucket region, eg. us-east-1
|
||||
s3_region = "env(S3_REGION)"
|
||||
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
||||
s3_access_key = "env(S3_ACCESS_KEY)"
|
||||
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
||||
s3_secret_key = "env(S3_SECRET_KEY)"
|
||||
@@ -1,102 +0,0 @@
|
||||
-- Create roles table
|
||||
CREATE TABLE IF NOT EXISTS public.roles (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create user profiles table
|
||||
CREATE TABLE IF NOT EXISTS public.user_profiles (
|
||||
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create user roles junction table
|
||||
CREATE TABLE IF NOT EXISTS public.user_roles (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
role_id UUID REFERENCES public.roles(id) ON DELETE CASCADE,
|
||||
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
assigned_by UUID REFERENCES auth.users(id),
|
||||
UNIQUE(user_id, role_id)
|
||||
);
|
||||
|
||||
-- Insert default roles
|
||||
INSERT INTO public.roles (name, description) VALUES
|
||||
('admin', 'Administrator with full system access'),
|
||||
('user', 'Regular user with basic access'),
|
||||
('moderator', 'Moderator with limited administrative access')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Enable RLS on all tables
|
||||
ALTER TABLE public.roles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Roles table policies
|
||||
CREATE POLICY "Anyone can view roles" ON public.roles FOR SELECT USING (true);
|
||||
|
||||
CREATE POLICY "Only admins can manage roles" ON public.roles FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.user_roles ur
|
||||
JOIN public.roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = auth.uid() AND r.name = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- User profiles policies
|
||||
CREATE POLICY "Users can view their own profile" ON public.user_profiles FOR SELECT USING (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Users can update their own profile" ON public.user_profiles FOR UPDATE USING (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Users can insert their own profile" ON public.user_profiles FOR INSERT WITH CHECK (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Admins can view all profiles" ON public.user_profiles FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.user_roles ur
|
||||
JOIN public.roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = auth.uid() AND r.name = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- User roles policies
|
||||
CREATE POLICY "Users can view their own roles" ON public.user_roles FOR SELECT USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Admins can manage all user roles" ON public.user_roles FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.user_roles ur
|
||||
JOIN public.roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = auth.uid() AND r.name = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- Function to get user roles
|
||||
CREATE OR REPLACE FUNCTION get_user_roles(user_uuid UUID)
|
||||
RETURNS TABLE(role_name VARCHAR(50))
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
SELECT r.name
|
||||
FROM public.user_roles ur
|
||||
JOIN public.roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = user_uuid;
|
||||
$$;
|
||||
|
||||
-- Function to check if user has specific role
|
||||
CREATE OR REPLACE FUNCTION user_has_role(user_uuid UUID, role_name VARCHAR(50))
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.user_roles ur
|
||||
JOIN public.roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = user_uuid AND r.name = role_name
|
||||
);
|
||||
$$;
|
||||
Reference in New Issue
Block a user