Add migrations
This commit is contained in:
parent
373aaff892
commit
0abea08c4f
6 changed files with 819 additions and 0 deletions
360
sql/35_stripe_wrappers.sql
Normal file
360
sql/35_stripe_wrappers.sql
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
-- ============================================================================
|
||||
-- Stripe Integration - Custom Extensions
|
||||
-- ============================================================================
|
||||
-- This file adds custom functionality on top of @supabase/stripe-sync-engine
|
||||
-- The library creates and manages all stripe.* tables - we don't modify them!
|
||||
-- We only: create views, add profile integration, and helper functions
|
||||
-- ============================================================================
|
||||
-- IMPORTANT: Run the stripe-sync-engine migrations FIRST before running this!
|
||||
-- See: docs/STRIPE_WITH_SYNC_ENGINE.md
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- Functions to Access Stripe Data (Security Definer)
|
||||
-- ============================================================================
|
||||
-- Since we don't modify the stripe schema, we use security definer functions
|
||||
-- to access the data and filter by user_id from metadata
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to get user's customer record
|
||||
create or replace function public.get_user_stripe_customer()
|
||||
returns table (
|
||||
id text,
|
||||
email text,
|
||||
user_id uuid,
|
||||
created integer,
|
||||
metadata jsonb
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
c.id,
|
||||
c.email,
|
||||
(c.metadata->>'user_id')::uuid as user_id,
|
||||
c.created,
|
||||
c.metadata
|
||||
from stripe.customers c
|
||||
where (c.metadata->>'user_id')::uuid = auth.uid();
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Function to get user's subscriptions
|
||||
create or replace function public.get_user_stripe_subscriptions()
|
||||
returns table (
|
||||
id text,
|
||||
customer text,
|
||||
user_id uuid,
|
||||
status text,
|
||||
items jsonb,
|
||||
cancel_at_period_end boolean,
|
||||
current_period_start integer,
|
||||
current_period_end integer,
|
||||
created integer,
|
||||
canceled_at integer,
|
||||
trial_start jsonb,
|
||||
trial_end jsonb,
|
||||
metadata jsonb
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
s.id,
|
||||
s.customer,
|
||||
(c.metadata->>'user_id')::uuid as user_id,
|
||||
s.status::text,
|
||||
s.items,
|
||||
s.cancel_at_period_end,
|
||||
s.current_period_start,
|
||||
s.current_period_end,
|
||||
s.created,
|
||||
s.canceled_at,
|
||||
s.trial_start,
|
||||
s.trial_end,
|
||||
s.metadata
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
where (c.metadata->>'user_id')::uuid = auth.uid();
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Function to get all stripe products (public access)
|
||||
create or replace function public.get_stripe_products()
|
||||
returns table (
|
||||
id text,
|
||||
name text,
|
||||
description text,
|
||||
active boolean,
|
||||
created integer,
|
||||
metadata jsonb
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
p.id,
|
||||
p.name,
|
||||
p.description,
|
||||
p.active,
|
||||
p.created,
|
||||
p.metadata
|
||||
from stripe.products p
|
||||
where p.active = true;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Function to get all stripe prices (public access)
|
||||
create or replace function public.get_stripe_prices()
|
||||
returns table (
|
||||
id text,
|
||||
product text,
|
||||
active boolean,
|
||||
currency text,
|
||||
unit_amount integer,
|
||||
recurring jsonb,
|
||||
created integer,
|
||||
metadata jsonb
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
pr.id,
|
||||
pr.product,
|
||||
pr.active,
|
||||
pr.currency,
|
||||
pr.unit_amount,
|
||||
pr.recurring,
|
||||
pr.created,
|
||||
pr.metadata
|
||||
from stripe.prices pr
|
||||
where pr.active = true;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ============================================================================
|
||||
-- Add subscription status to profiles
|
||||
-- ============================================================================
|
||||
|
||||
-- Add is_paying column to profiles table
|
||||
alter table public.profiles
|
||||
add column if not exists is_paying boolean default false;
|
||||
|
||||
-- Add subscription_tier column to profiles table
|
||||
alter table public.profiles
|
||||
add column if not exists subscription_tier text default 'free';
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to check if a user is a paying subscriber
|
||||
create or replace function public.is_paying_user(user_uuid uuid)
|
||||
returns boolean
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
has_active_subscription boolean;
|
||||
begin
|
||||
select exists(
|
||||
select 1
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
where (c.metadata->>'user_id')::uuid = user_uuid
|
||||
and s.status::text in ('active', 'trialing')
|
||||
and to_timestamp(s.current_period_end) > now()
|
||||
) into has_active_subscription;
|
||||
|
||||
return has_active_subscription;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Function to get user's subscription status
|
||||
create or replace function public.get_user_subscription_status(user_uuid uuid)
|
||||
returns table (
|
||||
subscription_id text,
|
||||
status text,
|
||||
current_period_end integer,
|
||||
cancel_at_period_end boolean,
|
||||
items jsonb,
|
||||
product_name text
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
first_item jsonb;
|
||||
price_id text;
|
||||
begin
|
||||
return query
|
||||
select
|
||||
s.id,
|
||||
s.status::text,
|
||||
s.current_period_end,
|
||||
s.cancel_at_period_end,
|
||||
s.items,
|
||||
p.name as product_name
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
left join lateral (
|
||||
select (s.items->'data'->0->>'price')::text as extracted_price_id
|
||||
) price_extract on true
|
||||
left join stripe.prices pr on pr.id = price_extract.extracted_price_id
|
||||
left join stripe.products p on p.id = pr.product
|
||||
where (c.metadata->>'user_id')::uuid = user_uuid
|
||||
and s.status::text in ('active', 'trialing', 'past_due')
|
||||
order by s.current_period_end desc
|
||||
limit 1;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Function to get user's Stripe customer ID
|
||||
create or replace function public.get_user_stripe_customer_id(user_uuid uuid)
|
||||
returns text
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
customer_id text;
|
||||
begin
|
||||
select id into customer_id
|
||||
from stripe.customers
|
||||
where (metadata->>'user_id')::uuid = user_uuid
|
||||
limit 1;
|
||||
|
||||
return customer_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Function to update profile subscription status
|
||||
-- Drop the function to update profile subscription status
|
||||
drop function if exists public.update_profile_subscription_status();
|
||||
create function public.update_profile_subscription_status()
|
||||
returns trigger as $$
|
||||
declare
|
||||
v_user_id uuid;
|
||||
v_customer_id text;
|
||||
begin
|
||||
-- Get customer ID based on which table triggered this
|
||||
if TG_TABLE_NAME = 'subscriptions' then
|
||||
v_customer_id := new.customer;
|
||||
elsif TG_TABLE_NAME = 'subscription_items' then
|
||||
-- Get customer ID from the subscription
|
||||
select customer into v_customer_id
|
||||
from stripe.subscriptions
|
||||
where id = new.subscription;
|
||||
else
|
||||
-- Unknown table, skip
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- Skip if no customer_id found
|
||||
if v_customer_id is null then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- Extract user_id from customer metadata
|
||||
select (metadata->>'user_id')::uuid into v_user_id
|
||||
from stripe.customers
|
||||
where id = v_customer_id;
|
||||
|
||||
-- Skip if no user_id found
|
||||
if v_user_id is null then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- Update the user's profile when subscription changes
|
||||
update public.profiles
|
||||
set
|
||||
is_paying = (
|
||||
select exists(
|
||||
select 1
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
where (c.metadata->>'user_id')::uuid = v_user_id
|
||||
and s.status::text in ('active', 'trialing')
|
||||
and to_timestamp(s.current_period_end) > now()
|
||||
)
|
||||
),
|
||||
subscription_tier = (
|
||||
case
|
||||
when new.status::text in ('active', 'trialing') then 'standard'
|
||||
else 'free'
|
||||
end
|
||||
)
|
||||
where id = v_user_id;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
-- Trigger to update profile when subscription changes
|
||||
drop trigger if exists update_profile_on_subscription_change on stripe.subscriptions;
|
||||
create trigger update_profile_on_subscription_change
|
||||
after insert or update on stripe.subscriptions
|
||||
for each row
|
||||
execute function public.update_profile_subscription_status();
|
||||
|
||||
drop trigger if exists update_profile_on_subscription_item_change on stripe.subscription_items;
|
||||
create trigger update_profile_on_subscription_item_change
|
||||
after insert or update on stripe.subscription_items
|
||||
for each row
|
||||
execute function public.update_profile_subscription_status();
|
||||
|
||||
-- ============================================================================
|
||||
-- Views for convenience
|
||||
-- ============================================================================
|
||||
|
||||
-- View for active subscriptions with user info
|
||||
create or replace view public.active_subscriptions as
|
||||
select
|
||||
s.id as subscription_id,
|
||||
(c.metadata->>'user_id')::uuid as user_id,
|
||||
p.email as user_email,
|
||||
p.first_name,
|
||||
p.last_name,
|
||||
s.status::text,
|
||||
to_timestamp(s.current_period_start) as current_period_start,
|
||||
to_timestamp(s.current_period_end) as current_period_end,
|
||||
s.cancel_at_period_end,
|
||||
pr.name as product_name,
|
||||
pc.currency,
|
||||
pc.unit_amount,
|
||||
pc.recurring->>'interval' as interval
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
inner join public.profiles p on p.id = (c.metadata->>'user_id')::uuid
|
||||
left join lateral (
|
||||
select (s.items->'data'->0->>'price')::text as extracted_price_id
|
||||
) price_extract on true
|
||||
left join stripe.prices pc on pc.id = price_extract.extracted_price_id
|
||||
left join stripe.products pr on pr.id = pc.product
|
||||
where s.status::text in ('active', 'trialing')
|
||||
and to_timestamp(s.current_period_end) > now()
|
||||
and (c.metadata->>'user_id') is not null;
|
||||
|
||||
-- ============================================================================
|
||||
-- Comments for documentation
|
||||
-- ============================================================================
|
||||
|
||||
comment on view public.active_subscriptions is 'Active subscriptions joined with user profile data';
|
||||
|
||||
comment on function public.get_user_stripe_customer is 'Returns current user''s customer record from Stripe';
|
||||
comment on function public.get_user_stripe_subscriptions is 'Returns current user''s subscriptions from Stripe';
|
||||
comment on function public.get_stripe_products is 'Returns all active Stripe products (public access)';
|
||||
comment on function public.get_stripe_prices is 'Returns all active Stripe prices (public access)';
|
||||
comment on function public.is_paying_user is 'Returns true if user has an active or trialing subscription';
|
||||
comment on function public.get_user_subscription_status is 'Returns current subscription details for a user';
|
||||
comment on function public.get_user_stripe_customer_id is 'Returns the Stripe customer ID for a user';
|
||||
comment on function public.update_profile_subscription_status is 'Trigger function to update profile fields when subscription changes';
|
||||
|
||||
290
sql/36_fix_stripe_subscription_dates.sql
Normal file
290
sql/36_fix_stripe_subscription_dates.sql
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
-- ============================================================================
|
||||
-- Fix Stripe Subscription Period Dates
|
||||
-- ============================================================================
|
||||
-- This migration fixes the subscription logic to use subscription_items
|
||||
-- for current_period_end (the actual billing cycle end date) instead of
|
||||
-- subscriptions.current_period_end (which is NULL for ongoing subscriptions)
|
||||
--
|
||||
-- Changes:
|
||||
-- 1. Remove is_paying column from profiles
|
||||
-- 2. Add plan enum column to profiles (trial, standard, none)
|
||||
-- 3. Update all functions to use subscription_items.current_period_end
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- Create plan enum type
|
||||
-- ============================================================================
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (select 1 from pg_type where typname = 'subscription_plan') then
|
||||
create type subscription_plan as enum ('none', 'trial', 'standard');
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- Update profiles table
|
||||
-- ============================================================================
|
||||
|
||||
-- Remove is_paying column
|
||||
alter table public.profiles
|
||||
drop column if exists is_paying;
|
||||
|
||||
-- Remove subscription_tier column (replaced with plan)
|
||||
alter table public.profiles
|
||||
drop column if exists subscription_tier;
|
||||
|
||||
-- Add plan column
|
||||
alter table public.profiles
|
||||
add column if not exists plan subscription_plan default 'none';
|
||||
|
||||
-- ============================================================================
|
||||
-- Updated Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to check if a user is a paying subscriber (using subscription_items)
|
||||
create or replace function public.is_paying_user(user_uuid uuid)
|
||||
returns boolean
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
has_active_subscription boolean;
|
||||
begin
|
||||
select exists(
|
||||
select 1
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
inner join stripe.subscription_items si on si.subscription = s.id
|
||||
where (c.metadata->>'user_id')::uuid = user_uuid
|
||||
and s.status::text in ('active', 'trialing')
|
||||
and si.current_period_end is not null
|
||||
and to_timestamp(si.current_period_end) > now()
|
||||
) into has_active_subscription;
|
||||
|
||||
return has_active_subscription;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Function to get user's subscription status (using subscription_items)
|
||||
create or replace function public.get_user_subscription_status(user_uuid uuid)
|
||||
returns table (
|
||||
subscription_id text,
|
||||
status text,
|
||||
current_period_start integer,
|
||||
current_period_end integer,
|
||||
cancel_at_period_end boolean,
|
||||
price_id text,
|
||||
product_name text,
|
||||
plan subscription_plan
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
s.id,
|
||||
s.status::text,
|
||||
si.current_period_start,
|
||||
si.current_period_end,
|
||||
s.cancel_at_period_end,
|
||||
si.price as price_id,
|
||||
p.name as product_name,
|
||||
case
|
||||
when s.status::text = 'trialing' then 'trial'::subscription_plan
|
||||
when s.status::text in ('active', 'past_due') then 'standard'::subscription_plan
|
||||
else 'none'::subscription_plan
|
||||
end as plan
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
inner join stripe.subscription_items si on si.subscription = s.id
|
||||
left join stripe.prices pr on pr.id = si.price
|
||||
left join stripe.products p on p.id = pr.product
|
||||
where (c.metadata->>'user_id')::uuid = user_uuid
|
||||
and s.status::text in ('active', 'trialing', 'past_due')
|
||||
and si.current_period_end is not null
|
||||
order by si.current_period_end desc
|
||||
limit 1;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ============================================================================
|
||||
-- Updated Trigger Function
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to update profile subscription plan
|
||||
create or replace function public.update_profile_subscription_status()
|
||||
returns trigger as $$
|
||||
declare
|
||||
v_user_id uuid;
|
||||
v_plan subscription_plan;
|
||||
v_customer_id text;
|
||||
begin
|
||||
-- Get customer ID based on which table triggered this
|
||||
if TG_TABLE_NAME = 'subscriptions' then
|
||||
v_customer_id := new.customer;
|
||||
elsif TG_TABLE_NAME = 'subscription_items' then
|
||||
-- Get customer ID from the subscription
|
||||
select customer into v_customer_id
|
||||
from stripe.subscriptions
|
||||
where id = new.subscription;
|
||||
else
|
||||
-- Unknown table, skip
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- Skip if no customer_id found
|
||||
if v_customer_id is null then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- Extract user_id from customer metadata
|
||||
select (metadata->>'user_id')::uuid into v_user_id
|
||||
from stripe.customers
|
||||
where id = v_customer_id;
|
||||
|
||||
-- Skip if no user_id found
|
||||
if v_user_id is null then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- Determine the user's current plan
|
||||
select
|
||||
case
|
||||
when exists(
|
||||
select 1
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
inner join stripe.subscription_items si on si.subscription = s.id
|
||||
where (c.metadata->>'user_id')::uuid = v_user_id
|
||||
and s.status::text = 'trialing'
|
||||
and si.current_period_end is not null
|
||||
and to_timestamp(si.current_period_end) > now()
|
||||
) then 'trial'::subscription_plan
|
||||
when exists(
|
||||
select 1
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
inner join stripe.subscription_items si on si.subscription = s.id
|
||||
where (c.metadata->>'user_id')::uuid = v_user_id
|
||||
and s.status::text in ('active', 'past_due')
|
||||
and si.current_period_end is not null
|
||||
and to_timestamp(si.current_period_end) > now()
|
||||
) then 'standard'::subscription_plan
|
||||
else 'none'::subscription_plan
|
||||
end into v_plan;
|
||||
|
||||
-- Update the user's profile
|
||||
update public.profiles
|
||||
set plan = v_plan
|
||||
where id = v_user_id;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
-- Recreate trigger (it already exists from previous migration)
|
||||
drop trigger if exists update_profile_on_subscription_change on stripe.subscriptions;
|
||||
create trigger update_profile_on_subscription_change
|
||||
after insert or update on stripe.subscriptions
|
||||
for each row
|
||||
execute function public.update_profile_subscription_status();
|
||||
|
||||
-- Also create trigger on subscription_items since that's where period dates are
|
||||
drop trigger if exists update_profile_on_subscription_item_change on stripe.subscription_items;
|
||||
create trigger update_profile_on_subscription_item_change
|
||||
after insert or update on stripe.subscription_items
|
||||
for each row
|
||||
execute function public.update_profile_subscription_status();
|
||||
|
||||
-- ============================================================================
|
||||
-- Updated Views
|
||||
-- ============================================================================
|
||||
|
||||
-- View for active subscriptions with user info (using subscription_items)
|
||||
create or replace view public.active_subscriptions as
|
||||
select
|
||||
s.id as subscription_id,
|
||||
(c.metadata->>'user_id')::uuid as user_id,
|
||||
p.email as user_email,
|
||||
p.first_name,
|
||||
p.last_name,
|
||||
s.status::text,
|
||||
to_timestamp(si.current_period_start) as current_period_start,
|
||||
to_timestamp(si.current_period_end) as current_period_end,
|
||||
s.cancel_at_period_end,
|
||||
pr.name as product_name,
|
||||
pc.currency,
|
||||
pc.unit_amount,
|
||||
pc.recurring->>'interval' as interval,
|
||||
p.plan
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
inner join stripe.subscription_items si on si.subscription = s.id
|
||||
inner join public.profiles p on p.id = (c.metadata->>'user_id')::uuid
|
||||
left join stripe.prices pc on pc.id = si.price
|
||||
left join stripe.products pr on pr.id = pc.product
|
||||
where s.status::text in ('active', 'trialing')
|
||||
and si.current_period_end is not null
|
||||
and to_timestamp(si.current_period_end) > now()
|
||||
and (c.metadata->>'user_id') is not null;
|
||||
|
||||
-- ============================================================================
|
||||
-- Update RPC function to return subscription with correct period dates
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to get user's subscriptions (updated to use subscription_items)
|
||||
create or replace function public.get_user_stripe_subscriptions()
|
||||
returns table (
|
||||
id text,
|
||||
customer text,
|
||||
user_id uuid,
|
||||
status text,
|
||||
cancel_at_period_end boolean,
|
||||
current_period_start integer,
|
||||
current_period_end integer,
|
||||
created integer,
|
||||
canceled_at integer,
|
||||
trial_start jsonb,
|
||||
trial_end jsonb,
|
||||
price_id text,
|
||||
quantity integer,
|
||||
metadata jsonb
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
s.id,
|
||||
s.customer,
|
||||
(c.metadata->>'user_id')::uuid as user_id,
|
||||
s.status::text,
|
||||
s.cancel_at_period_end,
|
||||
si.current_period_start,
|
||||
si.current_period_end,
|
||||
s.created,
|
||||
s.canceled_at,
|
||||
s.trial_start,
|
||||
s.trial_end,
|
||||
si.price as price_id,
|
||||
si.quantity,
|
||||
s.metadata
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
left join stripe.subscription_items si on si.subscription = s.id
|
||||
where (c.metadata->>'user_id')::uuid = auth.uid()
|
||||
order by s.created desc;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ============================================================================
|
||||
-- Comments for documentation
|
||||
-- ============================================================================
|
||||
|
||||
comment on column public.profiles.plan is 'User subscription plan: none (free), trial, or standard';
|
||||
comment on function public.get_user_subscription_status is 'Returns current subscription details using subscription_items for accurate period dates';
|
||||
|
||||
|
||||
91
sql/37_secure_active_subscriptions.sql
Normal file
91
sql/37_secure_active_subscriptions.sql
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
-- ============================================================================
|
||||
-- Secure Active Subscriptions Access
|
||||
-- ============================================================================
|
||||
-- This migration fixes a security issue with the active_subscriptions view
|
||||
-- which could expose all users' subscription data.
|
||||
--
|
||||
-- Changes:
|
||||
-- 1. Drop the public.active_subscriptions view (insecure)
|
||||
-- 2. Create a secure function that only returns the current user's subscription
|
||||
-- 3. Add RLS-compatible function for admin access if needed
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- Drop the insecure view
|
||||
-- ============================================================================
|
||||
|
||||
drop view if exists public.active_subscriptions;
|
||||
|
||||
-- ============================================================================
|
||||
-- Create secure function to get current user's active subscription
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to get the authenticated user's active subscription
|
||||
create or replace function public.get_my_active_subscription()
|
||||
returns table (
|
||||
subscription_id text,
|
||||
user_id uuid,
|
||||
user_email text,
|
||||
first_name text,
|
||||
last_name text,
|
||||
status text,
|
||||
current_period_start timestamp with time zone,
|
||||
current_period_end timestamp with time zone,
|
||||
cancel_at_period_end boolean,
|
||||
product_name text,
|
||||
currency text,
|
||||
unit_amount integer,
|
||||
billing_interval text,
|
||||
plan subscription_plan
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
-- Set search path for security
|
||||
set search_path = public, stripe
|
||||
as $$
|
||||
begin
|
||||
-- Only return data for the authenticated user
|
||||
return query
|
||||
select
|
||||
s.id as subscription_id,
|
||||
(c.metadata->>'user_id')::uuid as user_id,
|
||||
p.email as user_email,
|
||||
p.first_name,
|
||||
p.last_name,
|
||||
s.status::text,
|
||||
to_timestamp(si.current_period_start) as current_period_start,
|
||||
to_timestamp(si.current_period_end) as current_period_end,
|
||||
s.cancel_at_period_end,
|
||||
pr.name as product_name,
|
||||
pc.currency,
|
||||
pc.unit_amount,
|
||||
pc.recurring->>'interval' as billing_interval,
|
||||
p.plan
|
||||
from stripe.subscriptions s
|
||||
inner join stripe.customers c on c.id = s.customer
|
||||
inner join stripe.subscription_items si on si.subscription = s.id
|
||||
inner join public.profiles p on p.id = (c.metadata->>'user_id')::uuid
|
||||
left join stripe.prices pc on pc.id = si.price
|
||||
left join stripe.products pr on pr.id = pc.product
|
||||
where (c.metadata->>'user_id')::uuid = auth.uid() -- Filter by authenticated user only!
|
||||
and s.status::text in ('active', 'trialing')
|
||||
and si.current_period_end is not null
|
||||
and to_timestamp(si.current_period_end) > now()
|
||||
order by si.current_period_end desc
|
||||
limit 1;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ============================================================================
|
||||
-- Grant appropriate permissions
|
||||
-- ============================================================================
|
||||
|
||||
-- Grant access to authenticated users for their own subscription
|
||||
grant execute on function public.get_my_active_subscription() to authenticated;
|
||||
|
||||
-- ============================================================================
|
||||
-- Comments for documentation
|
||||
-- ============================================================================
|
||||
|
||||
comment on function public.get_my_active_subscription is
|
||||
'Returns the current authenticated user''s active subscription (secure, RLS-compliant)';
|
||||
3
sql/38_remove_function.sql
Normal file
3
sql/38_remove_function.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
grant execute on function public.get_user_stripe_subscriptions() to authenticated;
|
||||
|
||||
drop function if exists public.get_my_active_subscriptions();
|
||||
7
sql/39_grant_access_to_schema.sql
Normal file
7
sql/39_grant_access_to_schema.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
GRANT USAGE ON SCHEMA stripe TO service_role;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA stripe TO service_role;
|
||||
GRANT ALL ON ALL ROUTINES IN SCHEMA stripe TO service_role;
|
||||
GRANT ALL ON ALL SEQUENCES IN SCHEMA stripe TO service_role;
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA stripe GRANT ALL ON TABLES TO service_role;
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA stripe GRANT ALL ON ROUTINES TO service_role;
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA stripe GRANT ALL ON SEQUENCES TO service_role;
|
||||
68
sql/cleanup_old_stripe_functions.sql
Normal file
68
sql/cleanup_old_stripe_functions.sql
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
-- ============================================================================
|
||||
-- Cleanup Script - Remove Old Custom Stripe Webhook Functions
|
||||
-- ============================================================================
|
||||
-- This removes functions that were created by the old 36_stripe_webhooks.sql
|
||||
-- They're no longer needed since we're using @supabase/stripe-sync-engine
|
||||
-- ============================================================================
|
||||
|
||||
-- Drop customer event handler functions
|
||||
drop function if exists public.handle_stripe_customer_created(text, text, uuid);
|
||||
drop function if exists public.handle_stripe_customer_updated(text, text);
|
||||
drop function if exists public.handle_stripe_customer_deleted(text);
|
||||
|
||||
-- Drop product event handler functions
|
||||
drop function if exists public.handle_stripe_product_upsert(text, text, text, boolean, text, jsonb);
|
||||
drop function if exists public.handle_stripe_product_deleted(text);
|
||||
|
||||
-- Drop price event handler functions
|
||||
drop function if exists public.handle_stripe_price_upsert(text, text, boolean, text, bigint, text, integer, integer, jsonb);
|
||||
drop function if exists public.handle_stripe_price_deleted(text);
|
||||
|
||||
-- Drop subscription event handler functions
|
||||
drop function if exists public.handle_stripe_subscription_upsert(
|
||||
text, text, text, text, integer, boolean,
|
||||
timestamp with time zone, timestamp with time zone,
|
||||
timestamp with time zone, timestamp with time zone, timestamp with time zone
|
||||
);
|
||||
drop function if exists public.handle_stripe_subscription_deleted(text);
|
||||
|
||||
-- ============================================================================
|
||||
-- Verify cleanup
|
||||
-- ============================================================================
|
||||
|
||||
-- Check if any old functions remain
|
||||
SELECT
|
||||
routine_name,
|
||||
routine_schema
|
||||
FROM information_schema.routines
|
||||
WHERE routine_schema = 'public'
|
||||
AND routine_name LIKE 'handle_stripe_%'
|
||||
ORDER BY routine_name;
|
||||
|
||||
-- Should return 0 rows if cleanup was successful
|
||||
|
||||
-- ============================================================================
|
||||
-- Keep these functions (they're still used)
|
||||
-- ============================================================================
|
||||
|
||||
-- ✅ is_paying_user(uuid) - Used by frontend
|
||||
-- ✅ get_user_subscription_status(uuid) - Used by frontend
|
||||
-- ✅ get_user_stripe_customer_id(uuid) - Used by API
|
||||
-- ✅ sync_subscription_user_id() - Trigger function for user_id sync
|
||||
-- ✅ update_profile_subscription_status() - Trigger function for profile updates
|
||||
-- ✅ update_updated_at_column() - Generic trigger function
|
||||
|
||||
SELECT
|
||||
routine_name,
|
||||
routine_type
|
||||
FROM information_schema.routines
|
||||
WHERE routine_schema IN ('public', 'stripe')
|
||||
AND (
|
||||
routine_name LIKE '%paying%'
|
||||
OR routine_name LIKE '%subscription%user%'
|
||||
OR routine_name LIKE '%profile%'
|
||||
)
|
||||
ORDER BY routine_name;
|
||||
|
||||
-- Should show the functions we want to keep
|
||||
|
||||
Loading…
Reference in a new issue