diff --git a/src/app/employees/actions.ts b/src/app/employees/actions.ts index 75ac1ff..49cd965 100644 --- a/src/app/employees/actions.ts +++ b/src/app/employees/actions.ts @@ -139,7 +139,7 @@ export async function updateEmployee(id: string, formData: FormData) { military_status: formData.get('military_status'), start_date: formData.get('start_date') || null, leave_date: formData.get('leave_date') || null, - status: formData.get('leave_date') ? 'inactive' : formData.get('status') + status: formData.get('leave_date') ? 'inactive' : (formData.get('status') || 'active') }) .eq('id', id) diff --git a/src/components/employees/EmployeeModal.tsx b/src/components/employees/EmployeeModal.tsx index b4dc8b3..79b722d 100644 --- a/src/components/employees/EmployeeModal.tsx +++ b/src/components/employees/EmployeeModal.tsx @@ -255,6 +255,14 @@ export default function EmployeeModal({ +
+ + +
diff --git a/src/utils/supabase/admin.ts b/src/utils/supabase/admin.ts index 277d2cf..faadb47 100644 --- a/src/utils/supabase/admin.ts +++ b/src/utils/supabase/admin.ts @@ -1,14 +1,20 @@ import { createClient } from '@supabase/supabase-js' export function createAdminClient() { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const key = process.env.SUPABASE_SERVICE_ROLE_KEY; + + console.log('Admin Client Init - URL:', url); + console.log('Admin Client Init - Key Length:', key?.length); + return createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY!, + url!, + key!, { auth: { autoRefreshToken: false, persistSession: false } } - ) + ); } diff --git a/supabase/migrations/20240319000001_fix_employees_rls.sql b/supabase/migrations/20240319000001_fix_employees_rls.sql new file mode 100644 index 0000000..f737ba8 --- /dev/null +++ b/supabase/migrations/20240319000001_fix_employees_rls.sql @@ -0,0 +1,94 @@ +-- Fix RLS Policies for Employees Table (V5 - Architectural Fix) +-- This version moves user context to the 'users' table to break recursion. + +-- 1. Extend public.users table (Idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='role_name') THEN + ALTER TABLE public.users ADD COLUMN role_name TEXT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='company_id') THEN + ALTER TABLE public.users ADD COLUMN company_id UUID; + END IF; +END $$; + +-- 2. Create Sync Function +CREATE OR REPLACE FUNCTION public.sync_user_security_context() +RETURNS TRIGGER AS $$ +BEGIN + -- Update the users table when an employee is linked to a user_id + IF NEW.user_id IS NOT NULL THEN + UPDATE public.users + SET + role_name = (SELECT name FROM public.roles WHERE id = NEW.role_id), + company_id = NEW.company_id + WHERE id = NEW.user_id; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 3. Create Sync Trigger +DROP TRIGGER IF EXISTS on_employee_security_change ON public.employees; +CREATE TRIGGER on_employee_security_change +AFTER INSERT OR UPDATE OF role_id, company_id, user_id ON public.employees +FOR EACH ROW +EXECUTE FUNCTION public.sync_user_security_context(); + +-- 4. Initial Sync (Seed existing users) +UPDATE public.users u +SET + role_name = r.name, + company_id = e.company_id +FROM public.employees e +JOIN public.roles r ON e.role_id = r.id +WHERE e.user_id = u.id; + +-- 5. Helper Function (Bypass RLS) +-- This one is now super safe because it only checks 'users' table +CREATE OR REPLACE FUNCTION public.is_authorized_to_manage(target_company_id UUID) +RETURNS BOOLEAN AS $$ +BEGIN + -- Allow if table is empty (initial setup) + IF NOT EXISTS (SELECT 1 FROM public.employees) THEN + RETURN TRUE; + END IF; + + -- Check users table for cached role info + -- Since we are querying 'users' from an 'employees' policy, there is NO RECURSION. + RETURN EXISTS ( + SELECT 1 FROM public.users + WHERE id = auth.uid() + AND (role_name = 'admin' OR (role_name = 'manager' AND company_id = target_company_id)) + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 6. Redefine Employees Policies +DROP POLICY IF EXISTS "Employees unified select" ON public.employees; +DROP POLICY IF EXISTS "Employees unified insert" ON public.employees; +DROP POLICY IF EXISTS "Employees unified update" ON public.employees; +DROP POLICY IF EXISTS "Employees unified delete" ON public.employees; +DROP POLICY IF EXISTS "View employees policy" ON public.employees; +DROP POLICY IF EXISTS "Manage employees policy" ON public.employees; +DROP POLICY IF EXISTS "Initial setup allowance" ON public.employees; + +-- SELECT: Colleagues can see each other, Admins see all +CREATE POLICY "Employees select policy" +ON public.employees +FOR SELECT TO authenticated +USING ( + user_id = auth.uid() -- Can see self + OR EXISTS ( + SELECT 1 FROM public.users u + WHERE u.id = auth.uid() + AND (u.role_name = 'admin' OR u.company_id = employees.company_id) + ) +); + +-- INSERT/UPDATE/DELETE +CREATE POLICY "Employees management policy" +ON public.employees +FOR ALL TO authenticated +USING ( public.is_authorized_to_manage(company_id) ) +WITH CHECK ( public.is_authorized_to_manage(company_id) );