fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
-- Migration 033: Admin Users CRUD
|
||||
--
|
||||
-- RPC functions for listing, creating, updating, and deactivating admin_users.
|
||||
-- Security constraints enforced at SQL level:
|
||||
-- - platform_admin: can manage all users and any brand
|
||||
-- - brand_admin with can_manage_users: can only manage users within their brand,
|
||||
-- cannot create platform_admin, cannot assign foreign brand_id
|
||||
-- - store_employee: cannot access these functions (guarded at TS route level)
|
||||
--
|
||||
-- Display name: joins auth.users raw_user_meta_data -> display_name | name, falls back to email.
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 1. get_admin_users — list users, optionally filtered by brand
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DROP FUNCTION IF EXISTS get_admin_users(UUID);
|
||||
CREATE OR REPLACE FUNCTION get_admin_users(p_brand_id UUID DEFAULT NULL)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
user_id UUID,
|
||||
display_name TEXT,
|
||||
email TEXT,
|
||||
role TEXT,
|
||||
brand_id UUID,
|
||||
brand_name TEXT,
|
||||
can_manage_products BOOLEAN,
|
||||
can_manage_stops BOOLEAN,
|
||||
can_manage_orders BOOLEAN,
|
||||
can_manage_pickup BOOLEAN,
|
||||
can_manage_messages BOOLEAN,
|
||||
can_manage_refunds BOOLEAN,
|
||||
can_manage_users BOOLEAN,
|
||||
can_manage_water_log BOOLEAN,
|
||||
can_manage_reports BOOLEAN,
|
||||
active BOOLEAN,
|
||||
created_at TIMESTAMPTZ,
|
||||
last_login TIMESTAMPTZ
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
au.id,
|
||||
au.user_id,
|
||||
COALESCE(
|
||||
(au.raw_user_meta_data->>'display_name')::TEXT,
|
||||
(au.raw_user_meta_data->>'full_name')::TEXT,
|
||||
u.email
|
||||
) AS display_name,
|
||||
u.email,
|
||||
au.role::TEXT,
|
||||
au.brand_id,
|
||||
b.name AS brand_name,
|
||||
au.can_manage_products,
|
||||
au.can_manage_stops,
|
||||
au.can_manage_orders,
|
||||
au.can_manage_pickup,
|
||||
au.can_manage_messages,
|
||||
au.can_manage_refunds,
|
||||
au.can_manage_users,
|
||||
au.can_manage_water_log,
|
||||
COALESCE(au.can_manage_reports, false) AS can_manage_reports,
|
||||
au.active,
|
||||
au.created_at,
|
||||
au.last_login
|
||||
FROM admin_users au
|
||||
JOIN auth.users u ON au.user_id = u.id
|
||||
LEFT JOIN brands b ON au.brand_id = b.id
|
||||
WHERE
|
||||
-- platform_admin sees all; brand_admin sees only their brand
|
||||
(
|
||||
EXISTS (
|
||||
SELECT 1 FROM admin_users caller
|
||||
WHERE caller.user_id = auth.uid()
|
||||
AND caller.role = 'platform_admin'
|
||||
)
|
||||
OR
|
||||
(
|
||||
EXISTS (
|
||||
SELECT 1 FROM admin_users caller
|
||||
WHERE caller.user_id = auth.uid()
|
||||
AND caller.role = 'brand_admin'
|
||||
AND caller.can_manage_users = true
|
||||
)
|
||||
AND (p_brand_id IS NULL OR au.brand_id = p_brand_id)
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1 FROM admin_users caller
|
||||
WHERE caller.user_id = auth.uid()
|
||||
AND caller.brand_id = au.brand_id
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY au.created_at DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 2. create_admin_user — create a new admin user record
|
||||
--
|
||||
-- p_email: must already have a Supabase auth account
|
||||
-- p_role: 'platform_admin' | 'brand_admin' | 'store_employee'
|
||||
-- p_brand_id: required for brand_admin and store_employee
|
||||
-- p_flags: JSONB with individual can_manage_* booleans
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DROP FUNCTION IF EXISTS create_admin_user(TEXT, TEXT, UUID, JSONB);
|
||||
CREATE OR REPLACE FUNCTION create_admin_user(
|
||||
p_email TEXT,
|
||||
p_role TEXT,
|
||||
p_brand_id UUID DEFAULT NULL,
|
||||
p_flags JSONB DEFAULT '{}'::JSONB
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
user_id UUID,
|
||||
display_name TEXT,
|
||||
email TEXT,
|
||||
role TEXT,
|
||||
brand_id UUID,
|
||||
active BOOLEAN,
|
||||
created_at TIMESTAMPTZ
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_caller_role TEXT;
|
||||
v_caller_brand_id UUID;
|
||||
v_caller_can_manage_users BOOLEAN;
|
||||
v_target_user_id UUID;
|
||||
v_new_id UUID;
|
||||
BEGIN
|
||||
-- Caller must be authenticated
|
||||
IF auth.uid() IS NULL THEN
|
||||
RAISE EXCEPTION 'Not authenticated';
|
||||
END IF;
|
||||
|
||||
-- Look up caller
|
||||
SELECT role, brand_id, can_manage_users
|
||||
INTO v_caller_role, v_caller_brand_id, v_caller_can_manage_users
|
||||
FROM admin_users
|
||||
WHERE user_id = auth.uid();
|
||||
|
||||
IF v_caller_role IS NULL THEN
|
||||
RAISE EXCEPTION 'Not an admin';
|
||||
END IF;
|
||||
|
||||
-- Security: brand_admin cannot create platform_admin
|
||||
IF v_caller_role = 'brand_admin' AND p_role = 'platform_admin' THEN
|
||||
RAISE EXCEPTION 'Insufficient permissions to create a platform_admin';
|
||||
END IF;
|
||||
|
||||
-- Security: brand_admin can only create users in their own brand
|
||||
IF v_caller_role = 'brand_admin' THEN
|
||||
IF p_brand_id IS DISTINCT FROM v_caller_brand_id THEN
|
||||
RAISE EXCEPTION 'Insufficient permissions to create a user for another brand';
|
||||
END IF;
|
||||
IF p_brand_id IS NULL AND p_role = 'platform_admin' THEN
|
||||
RAISE EXCEPTION 'brand_admin cannot create platform_admin';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Find the auth user by email
|
||||
BEGIN
|
||||
SELECT id INTO v_target_user_id FROM auth.users WHERE email = p_email;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE EXCEPTION 'Auth user with email % not found', p_email;
|
||||
END;
|
||||
|
||||
IF v_target_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Auth user with email % not found', p_email;
|
||||
END IF;
|
||||
|
||||
-- Check no existing admin_users record for this user
|
||||
IF EXISTS (SELECT 1 FROM admin_users WHERE user_id = v_target_user_id) THEN
|
||||
RAISE EXCEPTION 'Admin user with email % already exists', p_email;
|
||||
END IF;
|
||||
|
||||
-- Insert new admin user
|
||||
INSERT INTO admin_users (
|
||||
user_id, role, brand_id,
|
||||
can_manage_products, can_manage_stops, can_manage_orders, can_manage_pickup,
|
||||
can_manage_messages, can_manage_refunds, can_manage_users, can_manage_water_log,
|
||||
can_manage_reports, active
|
||||
) VALUES (
|
||||
v_target_user_id, p_role, p_brand_id,
|
||||
COALESCE((p_flags->>'can_manage_products')::BOOLEAN, false),
|
||||
COALESCE((p_flags->>'can_manage_stops')::BOOLEAN, false),
|
||||
COALESCE((p_flags->>'can_manage_orders')::BOOLEAN, false),
|
||||
COALESCE((p_flags->>'can_manage_pickup')::BOOLEAN, false),
|
||||
COALESCE((p_flags->>'can_manage_messages')::BOOLEAN, false),
|
||||
COALESCE((p_flags->>'can_manage_refunds')::BOOLEAN, false),
|
||||
COALESCE((p_flags->>'can_manage_users')::BOOLEAN, false),
|
||||
COALESCE((p_flags->>'can_manage_water_log')::BOOLEAN, false),
|
||||
COALESCE((p_flags->>'can_manage_reports')::BOOLEAN, false),
|
||||
true
|
||||
)
|
||||
RETURNING id INTO v_new_id;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
au.id, au.user_id,
|
||||
COALESCE(
|
||||
(au.raw_user_meta_data->>'display_name')::TEXT,
|
||||
(au.raw_user_meta_data->>'full_name')::TEXT,
|
||||
u.email
|
||||
) AS display_name,
|
||||
u.email, au.role::TEXT, au.brand_id, au.active, au.created_at
|
||||
FROM admin_users au
|
||||
JOIN auth.users u ON au.user_id = u.id
|
||||
WHERE au.id = v_new_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 3. update_admin_user — update role, brand, flags, or active status
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DROP FUNCTION IF EXISTS update_admin_user(UUID, TEXT, UUID, JSONB, BOOLEAN);
|
||||
CREATE OR REPLACE FUNCTION update_admin_user(
|
||||
p_id UUID,
|
||||
p_role TEXT DEFAULT NULL,
|
||||
p_brand_id UUID DEFAULT NULL,
|
||||
p_flags JSONB DEFAULT NULL,
|
||||
p_active BOOLEAN DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
user_id UUID,
|
||||
display_name TEXT,
|
||||
email TEXT,
|
||||
role TEXT,
|
||||
brand_id UUID,
|
||||
active BOOLEAN,
|
||||
updated_at TIMESTAMPTZ
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_caller_role TEXT;
|
||||
v_caller_brand_id UUID;
|
||||
v_caller_can_manage_users BOOLEAN;
|
||||
v_target_role TEXT;
|
||||
v_target_brand_id UUID;
|
||||
BEGIN
|
||||
IF auth.uid() IS NULL THEN
|
||||
RAISE EXCEPTION 'Not authenticated';
|
||||
END IF;
|
||||
|
||||
SELECT role, brand_id, can_manage_users
|
||||
INTO v_caller_role, v_caller_brand_id, v_caller_can_manage_users
|
||||
FROM admin_users
|
||||
WHERE user_id = auth.uid();
|
||||
|
||||
IF v_caller_role IS NULL THEN
|
||||
RAISE EXCEPTION 'Not an admin';
|
||||
END IF;
|
||||
|
||||
-- Cannot update your own account's role
|
||||
IF EXISTS (SELECT 1 FROM admin_users WHERE id = p_id AND user_id = auth.uid()) THEN
|
||||
RAISE EXCEPTION 'Cannot update your own admin account';
|
||||
END IF;
|
||||
|
||||
-- Look up target
|
||||
SELECT role, brand_id INTO v_target_role, v_target_brand_id
|
||||
FROM admin_users WHERE id = p_id;
|
||||
|
||||
IF v_target_role IS NULL THEN
|
||||
RAISE EXCEPTION 'Admin user not found';
|
||||
END IF;
|
||||
|
||||
-- Security: brand_admin cannot demote/promote platform_admin
|
||||
IF v_caller_role = 'brand_admin' AND v_target_role = 'platform_admin' AND p_role IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Insufficient permissions to modify a platform_admin';
|
||||
END IF;
|
||||
|
||||
-- Security: brand_admin cannot give platform_admin role
|
||||
IF v_caller_role = 'brand_admin' AND p_role = 'platform_admin' THEN
|
||||
RAISE EXCEPTION 'Insufficient permissions to create a platform_admin';
|
||||
END IF;
|
||||
|
||||
-- Security: brand_admin can only affect users in their brand
|
||||
IF v_caller_role = 'brand_admin' THEN
|
||||
IF v_target_brand_id != v_caller_brand_id THEN
|
||||
RAISE EXCEPTION 'Insufficient permissions to modify a user from another brand';
|
||||
END IF;
|
||||
IF p_brand_id IS NOT NULL AND p_brand_id != v_caller_brand_id THEN
|
||||
RAISE EXCEPTION 'Insufficient permissions to assign a user to another brand';
|
||||
END IF;
|
||||
IF p_role = 'platform_admin' THEN
|
||||
RAISE EXCEPTION 'Insufficient permissions to create a platform_admin';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Apply updates
|
||||
UPDATE admin_users SET
|
||||
role = COALESCE(p_role, role),
|
||||
brand_id = COALESCE(p_brand_id, brand_id),
|
||||
can_manage_products = COALESCE((p_flags->>'can_manage_products')::BOOLEAN, can_manage_products),
|
||||
can_manage_stops = COALESCE((p_flags->>'can_manage_stops')::BOOLEAN, can_manage_stops),
|
||||
can_manage_orders = COALESCE((p_flags->>'can_manage_orders')::BOOLEAN, can_manage_orders),
|
||||
can_manage_pickup = COALESCE((p_flags->>'can_manage_pickup')::BOOLEAN, can_manage_pickup),
|
||||
can_manage_messages = COALESCE((p_flags->>'can_manage_messages')::BOOLEAN, can_manage_messages),
|
||||
can_manage_refunds = COALESCE((p_flags->>'can_manage_refunds')::BOOLEAN, can_manage_refunds),
|
||||
can_manage_users = COALESCE((p_flags->>'can_manage_users')::BOOLEAN, can_manage_users),
|
||||
can_manage_water_log = COALESCE((p_flags->>'can_manage_water_log')::BOOLEAN, can_manage_water_log),
|
||||
can_manage_reports = COALESCE((p_flags->>'can_manage_reports')::BOOLEAN, can_manage_reports),
|
||||
active = COALESCE(p_active, active)
|
||||
WHERE id = p_id;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
au.id, au.user_id,
|
||||
COALESCE(
|
||||
(au.raw_user_meta_data->>'display_name')::TEXT,
|
||||
(au.raw_user_meta_data->>'full_name')::TEXT,
|
||||
u.email
|
||||
) AS display_name,
|
||||
u.email, au.role::TEXT, au.brand_id, au.active, NOW() AS updated_at
|
||||
FROM admin_users au
|
||||
JOIN auth.users u ON au.user_id = u.id
|
||||
WHERE au.id = p_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 4. delete_admin_user — remove admin user record
|
||||
-- Cannot delete your own account.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DROP FUNCTION IF EXISTS delete_admin_user(UUID);
|
||||
CREATE OR REPLACE FUNCTION delete_admin_user(p_id UUID)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_caller_role TEXT;
|
||||
v_target_user_id UUID;
|
||||
BEGIN
|
||||
IF auth.uid() IS NULL THEN
|
||||
RAISE EXCEPTION 'Not authenticated';
|
||||
END IF;
|
||||
|
||||
SELECT role INTO v_caller_role FROM admin_users WHERE user_id = auth.uid();
|
||||
IF v_caller_role IS NULL THEN
|
||||
RAISE EXCEPTION 'Not an admin';
|
||||
END IF;
|
||||
|
||||
-- Cannot delete self
|
||||
SELECT user_id INTO v_target_user_id FROM admin_users WHERE id = p_id;
|
||||
IF v_target_user_id = auth.uid() THEN
|
||||
RAISE EXCEPTION 'Cannot delete your own admin account';
|
||||
END IF;
|
||||
|
||||
-- brand_admin can only delete users in their brand
|
||||
IF v_caller_role = 'brand_admin' THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM admin_users
|
||||
WHERE id = p_id AND brand_id = (SELECT brand_id FROM admin_users WHERE user_id = auth.uid())
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Insufficient permissions to delete this user';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
DELETE FROM admin_users WHERE id = p_id;
|
||||
RETURN true;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 5. Refresh PostgREST schema cache
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user