Revert "RBAC in place. Tailwind CSS working."

This reverts commit 90d874b15f.
This commit is contained in:
Alireza Vaezi
2025-07-18 21:18:07 -04:00
parent 90d874b15f
commit 033229989a
11 changed files with 96 additions and 1119 deletions

View File

@@ -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>
</>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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
View File

@@ -1,8 +0,0 @@
# Supabase
.branches
.temp
# dotenvx
.env.keys
.env.local
.env.*.local

View File

@@ -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)"

View File

@@ -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
);
$$;