mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
|
||||
-- Create vault secret for ACH encryption (idempotent)
|
||||
DO $$
|
||||
DECLARE v_exists boolean;
|
||||
BEGIN
|
||||
SELECT EXISTS(SELECT 1 FROM vault.secrets WHERE name = 'vendor_ach_encryption_key') INTO v_exists;
|
||||
IF NOT v_exists THEN
|
||||
PERFORM vault.create_secret(encode(extensions.gen_random_bytes(32), 'hex'), 'vendor_ach_encryption_key', 'Symmetric key for vendor ACH field encryption');
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Helper to fetch the key
|
||||
CREATE OR REPLACE FUNCTION public._get_vendor_ach_key()
|
||||
RETURNS text
|
||||
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'vendor_ach_encryption_key' LIMIT 1;
|
||||
$$;
|
||||
REVOKE ALL ON FUNCTION public._get_vendor_ach_key() FROM public, anon, authenticated;
|
||||
|
||||
-- Add encrypted (bytea) columns
|
||||
ALTER TABLE public.vendors
|
||||
ADD COLUMN IF NOT EXISTS ach_bank_name_enc bytea,
|
||||
ADD COLUMN IF NOT EXISTS ach_account_holder_enc bytea,
|
||||
ADD COLUMN IF NOT EXISTS ach_routing_number_enc bytea,
|
||||
ADD COLUMN IF NOT EXISTS ach_account_number_enc bytea,
|
||||
ADD COLUMN IF NOT EXISTS ach_account_last4 text;
|
||||
|
||||
-- Migrate any existing plaintext (likely none yet)
|
||||
UPDATE public.vendors
|
||||
SET ach_bank_name_enc = CASE WHEN ach_bank_name IS NOT NULL THEN extensions.pgp_sym_encrypt(ach_bank_name, public._get_vendor_ach_key()) END,
|
||||
ach_account_holder_enc = CASE WHEN ach_account_holder IS NOT NULL THEN extensions.pgp_sym_encrypt(ach_account_holder, public._get_vendor_ach_key()) END,
|
||||
ach_routing_number_enc = CASE WHEN ach_routing_number IS NOT NULL THEN extensions.pgp_sym_encrypt(ach_routing_number, public._get_vendor_ach_key()) END,
|
||||
ach_account_number_enc = CASE WHEN ach_account_number IS NOT NULL THEN extensions.pgp_sym_encrypt(ach_account_number, public._get_vendor_ach_key()) END,
|
||||
ach_account_last4 = CASE WHEN ach_account_number IS NOT NULL THEN RIGHT(ach_account_number, 4) END
|
||||
WHERE ach_bank_name IS NOT NULL OR ach_account_holder IS NOT NULL
|
||||
OR ach_routing_number IS NOT NULL OR ach_account_number IS NOT NULL;
|
||||
|
||||
-- Drop plaintext columns
|
||||
ALTER TABLE public.vendors
|
||||
DROP COLUMN IF EXISTS ach_bank_name,
|
||||
DROP COLUMN IF EXISTS ach_account_holder,
|
||||
DROP COLUMN IF EXISTS ach_routing_number,
|
||||
DROP COLUMN IF EXISTS ach_account_number;
|
||||
|
||||
-- Updated submit RPC: encrypts ACH inputs before saving
|
||||
CREATE OR REPLACE FUNCTION public.submit_vendor_profile(
|
||||
p_token text,
|
||||
p_remittance_address text,
|
||||
p_tax_id text,
|
||||
p_is_1099_eligible boolean,
|
||||
p_w9_document_url text DEFAULT NULL,
|
||||
p_insurance_carrier text DEFAULT NULL,
|
||||
p_insurance_policy_number text DEFAULT NULL,
|
||||
p_insurance_expiration_date date DEFAULT NULL,
|
||||
p_insurance_document_url text DEFAULT NULL,
|
||||
p_ach_bank_name text DEFAULT NULL,
|
||||
p_ach_account_holder text DEFAULT NULL,
|
||||
p_ach_routing_number text DEFAULT NULL,
|
||||
p_ach_account_number text DEFAULT NULL,
|
||||
p_ach_account_type text DEFAULT NULL
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_request RECORD;
|
||||
v_key text;
|
||||
BEGIN
|
||||
SELECT * INTO v_request FROM public.vendor_profile_requests
|
||||
WHERE token = p_token AND expires_at > now() AND submitted_at IS NULL
|
||||
LIMIT 1;
|
||||
|
||||
IF v_request IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
v_key := public._get_vendor_ach_key();
|
||||
|
||||
UPDATE public.vendors
|
||||
SET remittance_address = COALESCE(NULLIF(p_remittance_address, ''), remittance_address),
|
||||
tax_id = COALESCE(NULLIF(p_tax_id, ''), tax_id),
|
||||
is_1099_eligible = COALESCE(p_is_1099_eligible, is_1099_eligible),
|
||||
w9_document_url = COALESCE(p_w9_document_url, w9_document_url),
|
||||
insurance_carrier = COALESCE(NULLIF(p_insurance_carrier, ''), insurance_carrier),
|
||||
insurance_policy_number = COALESCE(NULLIF(p_insurance_policy_number, ''), insurance_policy_number),
|
||||
insurance_expiration_date = COALESCE(p_insurance_expiration_date, insurance_expiration_date),
|
||||
insurance_document_url = COALESCE(p_insurance_document_url, insurance_document_url),
|
||||
ach_bank_name_enc = CASE WHEN NULLIF(p_ach_bank_name, '') IS NOT NULL THEN extensions.pgp_sym_encrypt(p_ach_bank_name, v_key) ELSE ach_bank_name_enc END,
|
||||
ach_account_holder_enc = CASE WHEN NULLIF(p_ach_account_holder, '') IS NOT NULL THEN extensions.pgp_sym_encrypt(p_ach_account_holder, v_key) ELSE ach_account_holder_enc END,
|
||||
ach_routing_number_enc = CASE WHEN NULLIF(p_ach_routing_number, '') IS NOT NULL THEN extensions.pgp_sym_encrypt(p_ach_routing_number, v_key) ELSE ach_routing_number_enc END,
|
||||
ach_account_number_enc = CASE WHEN NULLIF(p_ach_account_number, '') IS NOT NULL THEN extensions.pgp_sym_encrypt(p_ach_account_number, v_key) ELSE ach_account_number_enc END,
|
||||
ach_account_last4 = CASE WHEN NULLIF(p_ach_account_number, '') IS NOT NULL THEN RIGHT(p_ach_account_number, 4) ELSE ach_account_last4 END,
|
||||
ach_account_type = COALESCE(NULLIF(p_ach_account_type, ''), ach_account_type),
|
||||
profile_last_submitted_at = now(),
|
||||
updated_at = now()
|
||||
WHERE id = v_request.vendor_id;
|
||||
|
||||
UPDATE public.vendor_profile_requests
|
||||
SET submitted_at = now()
|
||||
WHERE id = v_request.id;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.submit_vendor_profile(text, text, text, boolean, text, text, text, date, text, text, text, text, text, text) TO anon, authenticated;
|
||||
|
||||
-- Admin/manager-only decryption RPC
|
||||
CREATE OR REPLACE FUNCTION public.get_vendor_ach_details(p_vendor_id uuid)
|
||||
RETURNS TABLE(
|
||||
bank_name text,
|
||||
account_holder text,
|
||||
routing_number text,
|
||||
account_number text,
|
||||
account_type text,
|
||||
account_last4 text
|
||||
)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
DECLARE v_key text;
|
||||
BEGIN
|
||||
IF NOT (has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'manager'::app_role)) THEN
|
||||
RAISE EXCEPTION 'Forbidden';
|
||||
END IF;
|
||||
v_key := public._get_vendor_ach_key();
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
CASE WHEN v.ach_bank_name_enc IS NOT NULL THEN extensions.pgp_sym_decrypt(v.ach_bank_name_enc, v_key) END,
|
||||
CASE WHEN v.ach_account_holder_enc IS NOT NULL THEN extensions.pgp_sym_decrypt(v.ach_account_holder_enc, v_key) END,
|
||||
CASE WHEN v.ach_routing_number_enc IS NOT NULL THEN extensions.pgp_sym_decrypt(v.ach_routing_number_enc, v_key) END,
|
||||
CASE WHEN v.ach_account_number_enc IS NOT NULL THEN extensions.pgp_sym_decrypt(v.ach_account_number_enc, v_key) END,
|
||||
v.ach_account_type,
|
||||
v.ach_account_last4
|
||||
FROM public.vendors v
|
||||
WHERE v.id = p_vendor_id;
|
||||
END;
|
||||
$$;
|
||||
REVOKE ALL ON FUNCTION public.get_vendor_ach_details(uuid) FROM public, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.get_vendor_ach_details(uuid) TO authenticated;
|
||||
Reference in New Issue
Block a user