+ )
+}
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..658f1c6
--- /dev/null
+++ b/src/contexts/AuthContext.tsx
@@ -0,0 +1,174 @@
+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(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 = ({ children }) => {
+ const [user, setUser] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ // Fetch user profile and roles
+ const fetchUserData = async (authUser: User): Promise => {
+ 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 (
+
+ {children}
+
+ )
+}
diff --git a/src/index.css b/src/index.css
index 08a3ac9..a461c50 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,68 +1 @@
-: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;
- }
-}
+@import "tailwindcss";
\ No newline at end of file
diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts
new file mode 100644
index 0000000..de3fd35
--- /dev/null
+++ b/src/lib/supabase.ts
@@ -0,0 +1,107 @@
+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
+ }
+ }
+}
diff --git a/src/types/auth.ts b/src/types/auth.ts
new file mode 100644
index 0000000..9d06821
--- /dev/null
+++ b/src/types/auth.ts
@@ -0,0 +1,43 @@
+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
+ hasRole: (roleName: string) => boolean
+ isAdmin: () => boolean
+ refreshUserData: () => Promise
+}
+
+export type RoleName = 'admin' | 'user' | 'moderator' | 'coordinator' | 'conductor' | 'analyst'
diff --git a/supabase/.gitignore b/supabase/.gitignore
new file mode 100644
index 0000000..ad9264f
--- /dev/null
+++ b/supabase/.gitignore
@@ -0,0 +1,8 @@
+# Supabase
+.branches
+.temp
+
+# dotenvx
+.env.keys
+.env.local
+.env.*.local
diff --git a/supabase/config.toml b/supabase/config.toml
new file mode 100644
index 0000000..04d9c30
--- /dev/null
+++ b/supabase/config.toml
@@ -0,0 +1,322 @@
+# 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:////"
+
+# 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. .s3-.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)"
diff --git a/supabase/migrations/20250717153538_setup_rbac.sql b/supabase/migrations/20250717153538_setup_rbac.sql
new file mode 100644
index 0000000..690e4e3
--- /dev/null
+++ b/supabase/migrations/20250717153538_setup_rbac.sql
@@ -0,0 +1,102 @@
+-- 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
+ );
+$$;
\ No newline at end of file