B2B-88: remove supabase copy

This commit is contained in:
devmc-ee
2025-06-09 13:08:23 +03:00
parent ae563d7227
commit c392f51988
55 changed files with 0 additions and 11422 deletions

View File

@@ -23,7 +23,6 @@
- markdoc.css - Styles for Markdoc Markdown files. - markdoc.css - Styles for Markdoc Markdown files.
- -
/ supabase - primary supabase / supabase - primary supabase
/ supabase copy - (temporary) a folder from starter with bunch of settings for database and emailing, that should be migrated to the primary supabase to enable some functionality like mfa, super-admin and etc. After it is done, the folder will be removed
/ tooling - a workspace package, used for generation packages in node_modules and provides global links for its data. The most important is typescript config / tooling - a workspace package, used for generation packages in node_modules and provides global links for its data. The most important is typescript config
/ utils / utils

View File

@@ -1,4 +0,0 @@
# Supabase
.branches
.temp
.env

View File

@@ -1,108 +0,0 @@
# A string used to distinguish different Supabase projects on the same host. Defaults to the working
# directory name when running `supabase init`.
project_id = "next-supabase-saas-kit-turbo"
[api]
# 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 storage are always included.
schemas = ["public", "storage", "graphql_public"]
# Extra schemas to add to the search_path of every request. public is always included.
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
[db]
# Port to use for the local database URL.
port = 54322
# 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 = 15
[studio]
# Port to use for Supabase Studio.
port = 54323
# 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]
# Port to use for the email testing server web interface.
port = 54324
smtp_port = 54325
pop3_port = 54326
[storage]
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"
[auth]
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://localhost:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["http://localhost:3000", "http://localhost:3000/auth/callback", "http://localhost:3000/update-password"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
# week).
jwt_expiry = 3600
# Allow/disallow new user signups to your project.
enable_signup = true
# Enable TOTP MFA
[auth.mfa.totp]
verify_enabled = true
enroll_enabled = true
[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 = true
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
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 = ""
[auth.rate_limit]
email_sent = 1000
[auth.email.template.invite]
subject = "You are invited to Makerkit"
content_path = "./supabase/templates/invite-user.html"
[auth.email.template.confirmation]
subject = "Confirm your email"
content_path = "./supabase/templates/confirm-email.html"
[auth.email.template.recovery]
subject = "Reset your password"
content_path = "./supabase/templates/reset-password.html"
[auth.email.template.email_change]
subject = "Confirm your email change"
content_path = "./supabase/templates/change-email-address.html"
[auth.email.template.magic_link]
subject = "Sign in to Makerkit"
content_path = "./supabase/templates/magic-link.html"
[analytics]
enabled = false
[db.migrations]
schema_paths = [
"./schemas/*.sql",
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
-- Seed the roles table with default roles 'owner' and 'member'
insert into public.roles(
name,
hierarchy_level)
values (
'owner',
1);
insert into public.roles(
name,
hierarchy_level)
values (
'member',
2);
-- We seed the role_permissions table with the default roles and permissions
insert into public.role_permissions(
role,
permission)
values (
'owner',
'roles.manage'),
(
'owner',
'billing.manage'),
(
'owner',
'settings.manage'),
(
'owner',
'members.manage'),
(
'owner',
'invites.manage'),
(
'member',
'settings.manage'),
(
'member',
'invites.manage');

View File

@@ -1,7 +0,0 @@
create policy delete_team_account
on public.accounts
for delete
to authenticated
using (
auth.uid() = primary_owner_user_id
);

View File

@@ -1,346 +0,0 @@
create extension if not exists pg_cron;
-- Create a table to store one-time tokens (nonces)
CREATE TABLE IF NOT EXISTS public.nonces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_token TEXT NOT NULL, -- token sent to client (hashed)
nonce TEXT NOT NULL, -- token stored in DB (hashed)
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NULL, -- Optional to support anonymous tokens
purpose TEXT NOT NULL, -- e.g., 'password-reset', 'email-verification', etc.
-- Status fields
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
used_at TIMESTAMPTZ,
revoked BOOLEAN NOT NULL DEFAULT FALSE, -- For administrative revocation
revoked_reason TEXT, -- Reason for revocation if applicable
-- Audit fields
verification_attempts INTEGER NOT NULL DEFAULT 0, -- Track attempted uses
last_verification_at TIMESTAMPTZ, -- Timestamp of last verification attempt
last_verification_ip INET, -- For tracking verification source
last_verification_user_agent TEXT, -- For tracking client information
-- Extensibility fields
metadata JSONB DEFAULT '{}'::JSONB, -- optional metadata
scopes TEXT[] DEFAULT '{}' -- OAuth-style authorized scopes
);
-- Create indexes for efficient lookups
CREATE INDEX IF NOT EXISTS idx_nonces_status ON public.nonces (client_token, user_id, purpose, expires_at)
WHERE used_at IS NULL AND revoked = FALSE;
-- Enable Row Level Security (RLS)
ALTER TABLE public.nonces ENABLE ROW LEVEL SECURITY;
-- RLS policies
-- Users can view their own nonces for verification
CREATE POLICY "Users can read their own nonces"
ON public.nonces
FOR SELECT
USING (
user_id = (select auth.uid())
);
-- Create a function to create a nonce
CREATE OR REPLACE FUNCTION public.create_nonce(
p_user_id UUID DEFAULT NULL,
p_purpose TEXT DEFAULT NULL,
p_expires_in_seconds INTEGER DEFAULT 3600, -- 1 hour by default
p_metadata JSONB DEFAULT NULL,
p_scopes TEXT[] DEFAULT NULL,
p_revoke_previous BOOLEAN DEFAULT TRUE -- New parameter to control automatic revocation
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_client_token TEXT;
v_nonce TEXT;
v_expires_at TIMESTAMPTZ;
v_id UUID;
v_plaintext_token TEXT;
v_revoked_count INTEGER;
BEGIN
-- Revoke previous tokens for the same user and purpose if requested
-- This only applies if a user ID is provided (not for anonymous tokens)
IF p_revoke_previous = TRUE AND p_user_id IS NOT NULL THEN
WITH revoked AS (
UPDATE public.nonces
SET
revoked = TRUE,
revoked_reason = 'Superseded by new token with same purpose'
WHERE
user_id = p_user_id
AND purpose = p_purpose
AND used_at IS NULL
AND revoked = FALSE
AND expires_at > NOW()
RETURNING 1
)
SELECT COUNT(*) INTO v_revoked_count FROM revoked;
END IF;
-- Generate a 6-digit token
v_plaintext_token := (100000 + floor(random() * 900000))::text;
v_client_token := crypt(v_plaintext_token, gen_salt('bf'));
-- Still generate a secure nonce for internal use
v_nonce := encode(gen_random_bytes(24), 'base64');
v_nonce := crypt(v_nonce, gen_salt('bf'));
-- Calculate expiration time
v_expires_at := NOW() + (p_expires_in_seconds * interval '1 second');
-- Insert the new nonce
INSERT INTO public.nonces (
client_token,
nonce,
user_id,
expires_at,
metadata,
purpose,
scopes
)
VALUES (
v_client_token,
v_nonce,
p_user_id,
v_expires_at,
COALESCE(p_metadata, '{}'::JSONB),
p_purpose,
COALESCE(p_scopes, '{}'::TEXT[])
)
RETURNING id INTO v_id;
-- Return the token information
-- Note: returning the plaintext token, not the hash
RETURN jsonb_build_object(
'id', v_id,
'token', v_plaintext_token,
'expires_at', v_expires_at,
'revoked_previous_count', COALESCE(v_revoked_count, 0)
);
END;
$$;
grant execute on function public.create_nonce to service_role;
-- Create a function to verify a nonce
CREATE OR REPLACE FUNCTION public.verify_nonce(
p_token TEXT,
p_purpose TEXT,
p_user_id UUID DEFAULT NULL,
p_required_scopes TEXT[] DEFAULT NULL,
p_max_verification_attempts INTEGER DEFAULT 5,
p_ip INET DEFAULT NULL,
p_user_agent TEXT DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_nonce RECORD;
v_matching_count INTEGER;
BEGIN
-- Add debugging info
RAISE NOTICE 'Verifying token: %, purpose: %, user_id: %', p_token, p_purpose, p_user_id;
-- Count how many matching tokens exist before verification attempt
SELECT COUNT(*) INTO v_matching_count
FROM public.nonces
WHERE purpose = p_purpose;
-- Update verification attempt counter and tracking info for all matching tokens
UPDATE public.nonces
SET
verification_attempts = verification_attempts + 1,
last_verification_at = NOW(),
last_verification_ip = COALESCE(p_ip, last_verification_ip),
last_verification_user_agent = COALESCE(p_user_agent, last_verification_user_agent)
WHERE
client_token = crypt(p_token, client_token)
AND purpose = p_purpose;
-- Find the nonce by token and purpose
-- Modified to handle user-specific tokens better
SELECT *
INTO v_nonce
FROM public.nonces
WHERE
client_token = crypt(p_token, client_token)
AND purpose = p_purpose
-- Only apply user_id filter if the token was created for a specific user
AND (
-- Case 1: Anonymous token (user_id is NULL in DB)
(user_id IS NULL)
OR
-- Case 2: User-specific token (check if user_id matches)
(user_id = p_user_id)
)
AND used_at IS NULL
AND NOT revoked
AND expires_at > NOW();
-- Check if nonce exists
IF v_nonce.id IS NULL THEN
RETURN jsonb_build_object(
'valid', false,
'message', 'Invalid or expired token'
);
END IF;
-- Check if max verification attempts exceeded
IF p_max_verification_attempts > 0 AND v_nonce.verification_attempts > p_max_verification_attempts THEN
-- Automatically revoke the token
UPDATE public.nonces
SET
revoked = TRUE,
revoked_reason = 'Maximum verification attempts exceeded'
WHERE id = v_nonce.id;
RETURN jsonb_build_object(
'valid', false,
'message', 'Token revoked due to too many verification attempts',
'max_attempts_exceeded', true
);
END IF;
-- Check scopes if required
IF p_required_scopes IS NOT NULL AND array_length(p_required_scopes, 1) > 0 THEN
-- Fix scope validation to properly check if token scopes contain all required scopes
-- Using array containment check: array1 @> array2 (array1 contains array2)
IF NOT (v_nonce.scopes @> p_required_scopes) THEN
RETURN jsonb_build_object(
'valid', false,
'message', 'Token does not have required permissions',
'token_scopes', v_nonce.scopes,
'required_scopes', p_required_scopes
);
END IF;
END IF;
-- Mark nonce as used
UPDATE public.nonces
SET used_at = NOW()
WHERE id = v_nonce.id;
-- Return success with metadata
RETURN jsonb_build_object(
'valid', true,
'user_id', v_nonce.user_id,
'metadata', v_nonce.metadata,
'scopes', v_nonce.scopes,
'purpose', v_nonce.purpose
);
END;
$$;
grant execute on function public.verify_nonce to authenticated,service_role;
-- Create a function to revoke a nonce
CREATE OR REPLACE FUNCTION public.revoke_nonce(
p_id UUID,
p_reason TEXT DEFAULT NULL
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_affected_rows INTEGER;
BEGIN
UPDATE public.nonces
SET
revoked = TRUE,
revoked_reason = p_reason
WHERE
id = p_id
AND used_at IS NULL
AND NOT revoked
RETURNING 1 INTO v_affected_rows;
RETURN v_affected_rows > 0;
END;
$$;
grant execute on function public.revoke_nonce to service_role;
-- Create a function to clean up expired nonces
CREATE OR REPLACE FUNCTION kit.cleanup_expired_nonces(
p_older_than_days INTEGER DEFAULT 1,
p_include_used BOOLEAN DEFAULT TRUE,
p_include_revoked BOOLEAN DEFAULT TRUE
)
RETURNS INTEGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_count INTEGER;
BEGIN
-- Count and delete expired or used nonces based on parameters
WITH deleted AS (
DELETE FROM public.nonces
WHERE
(
-- Expired and unused tokens
(expires_at < NOW() AND used_at IS NULL)
-- Used tokens older than specified days (if enabled)
OR (p_include_used = TRUE AND used_at < NOW() - (p_older_than_days * interval '1 day'))
-- Revoked tokens older than specified days (if enabled)
OR (p_include_revoked = TRUE AND revoked = TRUE AND created_at < NOW() - (p_older_than_days * interval '1 day'))
)
RETURNING 1
)
SELECT COUNT(*) INTO v_count FROM deleted;
RETURN v_count;
END;
$$;
-- Create a function to get token status (for administrative use)
CREATE OR REPLACE FUNCTION public.get_nonce_status(
p_id UUID
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_nonce public.nonces;
BEGIN
SELECT * INTO v_nonce FROM public.nonces WHERE id = p_id;
IF v_nonce.id IS NULL THEN
RETURN jsonb_build_object('exists', false);
END IF;
RETURN jsonb_build_object(
'exists', true,
'purpose', v_nonce.purpose,
'user_id', v_nonce.user_id,
'created_at', v_nonce.created_at,
'expires_at', v_nonce.expires_at,
'used_at', v_nonce.used_at,
'revoked', v_nonce.revoked,
'revoked_reason', v_nonce.revoked_reason,
'verification_attempts', v_nonce.verification_attempts,
'last_verification_at', v_nonce.last_verification_at,
'last_verification_ip', v_nonce.last_verification_ip,
'is_valid', (v_nonce.used_at IS NULL AND NOT v_nonce.revoked AND v_nonce.expires_at > NOW())
);
END;
$$;
-- Comments for documentation
COMMENT ON TABLE public.nonces IS 'Table for storing one-time tokens with enhanced security and audit features';
COMMENT ON FUNCTION public.create_nonce IS 'Creates a new one-time token for a specific purpose with enhanced options';
COMMENT ON FUNCTION public.verify_nonce IS 'Verifies a one-time token, checks scopes, and marks it as used';
COMMENT ON FUNCTION public.revoke_nonce IS 'Administratively revokes a token to prevent its use';
COMMENT ON FUNCTION kit.cleanup_expired_nonces IS 'Cleans up expired, used, or revoked tokens based on parameters';
COMMENT ON FUNCTION public.get_nonce_status IS 'Retrieves the status of a token for administrative purposes';

View File

@@ -1,206 +0,0 @@
/*
* public.is_aal2
* Check if the user has aal2 access
*/
create
or replace function public.is_aal2() returns boolean
set
search_path = '' as
$$
declare
is_aal2 boolean;
begin
select auth.jwt() ->> 'aal' = 'aal2' into is_aal2;
return coalesce(is_aal2, false);
end
$$ language plpgsql;
-- Grant access to the function to authenticated users
grant execute on function public.is_aal2() to authenticated;
/*
* public.is_super_admin
* Check if the user is a super admin.
* A Super Admin is a user that has the role 'super-admin' and has MFA enabled.
*/
create
or replace function public.is_super_admin() returns boolean
set
search_path = '' as
$$
declare
is_super_admin boolean;
begin
if not public.is_aal2() then
return false;
end if;
select (auth.jwt() ->> 'app_metadata')::jsonb ->> 'role' = 'super-admin' into is_super_admin;
return coalesce(is_super_admin, false);
end
$$ language plpgsql;
-- Grant access to the function to authenticated users
grant execute on function public.is_super_admin() to authenticated;
/*
* public.is_mfa_compliant
* Check if the user meets MFA requirements if they have MFA enabled.
* If the user has MFA enabled, then the user must have aal2 enabled. Otherwise, the user must have aal1 enabled (default behavior).
*/
create or replace function public.is_mfa_compliant() returns boolean
set search_path = '' as
$$
begin
return array[(select auth.jwt()->>'aal')] <@ (
select
case
when count(id) > 0 then array['aal2']
else array['aal1', 'aal2']
end as aal
from auth.mfa_factors
where ((select auth.uid()) = auth.mfa_factors.user_id) and auth.mfa_factors.status = 'verified'
);
end
$$ language plpgsql security definer;
-- Grant access to the function to authenticated users
grant execute on function public.is_mfa_compliant() to authenticated;
-- MFA Restrictions:
-- the following policies are applied to the tables as a
-- restrictive policy to ensure that if MFA is enabled, then the policy will be applied.
-- For users that have not enabled MFA, the policy will not be applied and will keep the default behavior.
-- Restrict access to accounts if MFA is enabled
create policy restrict_mfa_accounts
on public.accounts
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to accounts memberships if MFA is enabled
create policy restrict_mfa_accounts_memberships
on public.accounts_memberships
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to subscriptions if MFA is enabled
create policy restrict_mfa_subscriptions
on public.subscriptions
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to subscription items if MFA is enabled
create policy restrict_mfa_subscription_items
on public.subscription_items
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to role permissions if MFA is enabled
create policy restrict_mfa_role_permissions
on public.role_permissions
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to invitations if MFA is enabled
create policy restrict_mfa_invitations
on public.invitations
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to orders if MFA is enabled
create policy restrict_mfa_orders
on public.orders
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to orders items if MFA is enabled
create policy restrict_mfa_order_items
on public.order_items
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to orders if MFA is enabled
create policy restrict_mfa_notifications
on public.notifications
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Super Admin:
-- the following policies are applied to the tables as a permissive policy to ensure that
-- super admins can access all tables (view only).
-- Allow Super Admins to access the accounts table
create policy super_admins_access_accounts
on public.accounts
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the accounts memberships table
create policy super_admins_access_accounts_memberships
on public.accounts_memberships
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the subscriptions table
create policy super_admins_access_subscriptions
on public.subscriptions
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the subscription items table
create policy super_admins_access_subscription_items
on public.subscription_items
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the invitations items table
create policy super_admins_access_invitations
on public.invitations
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the orders table
create policy super_admins_access_orders
on public.orders
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the order items table
create policy super_admins_access_order_items
on public.order_items
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the role permissions table
create policy super_admins_access_role_permissions
on public.role_permissions
as permissive
for select
to authenticated
using (public.is_super_admin());

View File

@@ -1,203 +0,0 @@
-- Create a function to create a nonce
create or replace function public.create_nonce (
p_user_id UUID default null,
p_purpose TEXT default null,
p_expires_in_seconds INTEGER default 3600, -- 1 hour by default
p_metadata JSONB default null,
p_scopes text[] default null,
p_revoke_previous BOOLEAN default true -- New parameter to control automatic revocation
) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER
set
search_path to '' as $$
DECLARE
v_client_token TEXT;
v_nonce TEXT;
v_expires_at TIMESTAMPTZ;
v_id UUID;
v_plaintext_token TEXT;
v_revoked_count INTEGER;
BEGIN
-- Revoke previous tokens for the same user and purpose if requested
-- This only applies if a user ID is provided (not for anonymous tokens)
IF p_revoke_previous = TRUE AND p_user_id IS NOT NULL THEN
WITH revoked AS (
UPDATE public.nonces
SET
revoked = TRUE,
revoked_reason = 'Superseded by new token with same purpose'
WHERE
user_id = p_user_id
AND purpose = p_purpose
AND used_at IS NULL
AND revoked = FALSE
AND expires_at > NOW()
RETURNING 1
)
SELECT COUNT(*) INTO v_revoked_count FROM revoked;
END IF;
-- Generate a 6-digit token
v_plaintext_token := (100000 + floor(random() * 900000))::text;
v_client_token := extensions.crypt(v_plaintext_token, extensions.gen_salt('bf'));
-- Still generate a secure nonce for internal use
v_nonce := encode(extensions.gen_random_bytes(24), 'base64');
v_nonce := extensions.crypt(v_nonce, extensions.gen_salt('bf'));
-- Calculate expiration time
v_expires_at := NOW() + (p_expires_in_seconds * interval '1 second');
-- Insert the new nonce
INSERT INTO public.nonces (
client_token,
nonce,
user_id,
expires_at,
metadata,
purpose,
scopes
)
VALUES (
v_client_token,
v_nonce,
p_user_id,
v_expires_at,
COALESCE(p_metadata, '{}'::JSONB),
p_purpose,
COALESCE(p_scopes, '{}'::TEXT[])
)
RETURNING id INTO v_id;
-- Return the token information
-- Note: returning the plaintext token, not the hash
RETURN jsonb_build_object(
'id', v_id,
'token', v_plaintext_token,
'expires_at', v_expires_at,
'revoked_previous_count', COALESCE(v_revoked_count, 0)
);
END;
$$;
grant
execute on function public.create_nonce to service_role;
-- Create a function to verify a nonce
--
create or replace function public.verify_nonce (
p_token TEXT,
p_purpose TEXT,
p_user_id UUID default null,
p_required_scopes text[] default null,
p_max_verification_attempts INTEGER default 5,
p_ip INET default null,
p_user_agent TEXT default null
) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER
set
SEARCH_PATH to '' as $$
DECLARE
v_nonce RECORD;
v_matching_count INTEGER;
BEGIN
-- Count how many matching tokens exist before verification attempt
SELECT COUNT(*)
INTO v_matching_count
FROM public.nonces
WHERE purpose = p_purpose;
-- Update verification attempt counter and tracking info for all matching tokens
UPDATE public.nonces
SET verification_attempts = verification_attempts + 1,
last_verification_at = NOW(),
last_verification_ip = COALESCE(p_ip, last_verification_ip),
last_verification_user_agent = COALESCE(p_user_agent, last_verification_user_agent)
WHERE client_token = extensions.crypt(p_token, client_token)
AND purpose = p_purpose;
-- Find the nonce by token and purpose
-- Modified to handle user-specific tokens better
SELECT *
INTO v_nonce
FROM public.nonces
WHERE client_token = extensions.crypt(p_token, client_token)
AND purpose = p_purpose
-- Only apply user_id filter if the token was created for a specific user
AND (
-- Case 1: Anonymous token (user_id is NULL in DB)
(user_id IS NULL)
OR
-- Case 2: User-specific token (check if user_id matches)
(user_id = p_user_id)
)
AND used_at IS NULL
AND NOT revoked
AND expires_at > NOW();
-- Check if nonce exists
IF v_nonce.id IS NULL THEN
RETURN jsonb_build_object(
'valid', false,
'message', 'Invalid or expired token'
);
END IF;
-- Check if max verification attempts exceeded
IF p_max_verification_attempts > 0 AND v_nonce.verification_attempts > p_max_verification_attempts THEN
-- Automatically revoke the token
UPDATE public.nonces
SET revoked = TRUE,
revoked_reason = 'Maximum verification attempts exceeded'
WHERE id = v_nonce.id;
RETURN jsonb_build_object(
'valid', false,
'message', 'Token revoked due to too many verification attempts',
'max_attempts_exceeded', true
);
END IF;
-- Check scopes if required
IF p_required_scopes IS NOT NULL AND array_length(p_required_scopes, 1) > 0 THEN
-- Fix scope validation to properly check if token scopes contain all required scopes
-- Using array containment check: array1 @> array2 (array1 contains array2)
IF NOT (v_nonce.scopes @> p_required_scopes) THEN
RETURN jsonb_build_object(
'valid', false,
'message', 'Token does not have required permissions',
'token_scopes', v_nonce.scopes,
'required_scopes', p_required_scopes
);
END IF;
END IF;
-- Mark nonce as used
UPDATE public.nonces
SET used_at = NOW()
WHERE id = v_nonce.id;
-- Return success with metadata
RETURN jsonb_build_object(
'valid', true,
'user_id', v_nonce.user_id,
'metadata', v_nonce.metadata,
'scopes', v_nonce.scopes,
'purpose', v_nonce.purpose
);
END;
$$;
grant
execute on function public.verify_nonce to authenticated,
service_role;
alter function public.revoke_nonce
set
search_path to '';
alter function kit.cleanup_expired_nonces
set
search_path to '';
alter function public.get_nonce_status
set
search_path to '';

View File

@@ -1,74 +0,0 @@
/*
* -------------------------------------------------------
* Section: Revoke default privileges from public schema
* We will revoke all default privileges from public schema on functions to prevent public access to them
* -------------------------------------------------------
*/
-- Create a private Makerkit schema
create schema if not exists kit;
create extension if not exists "unaccent" schema kit;
-- We remove all default privileges from public schema on functions to
-- prevent public access to them
alter default privileges
revoke
execute on functions
from
public;
revoke all on schema public
from
public;
revoke all PRIVILEGES on database "postgres"
from
"anon";
revoke all PRIVILEGES on schema "public"
from
"anon";
revoke all PRIVILEGES on schema "storage"
from
"anon";
revoke all PRIVILEGES on all SEQUENCES in schema "public"
from
"anon";
revoke all PRIVILEGES on all SEQUENCES in schema "storage"
from
"anon";
revoke all PRIVILEGES on all FUNCTIONS in schema "public"
from
"anon";
revoke all PRIVILEGES on all FUNCTIONS in schema "storage"
from
"anon";
revoke all PRIVILEGES on all TABLES in schema "public"
from
"anon";
revoke all PRIVILEGES on all TABLES in schema "storage"
from
"anon";
-- We remove all default privileges from public schema on functions to
-- prevent public access to them by default
alter default privileges in schema public
revoke
execute on functions
from
anon,
authenticated;
-- we allow the authenticated role to execute functions in the public schema
grant usage on schema public to authenticated;
-- we allow the service_role role to execute functions in the public schema
grant usage on schema public to service_role;

View File

@@ -1,65 +0,0 @@
/*
* -------------------------------------------------------
* Section: Enums
* We create the enums for the schema
* -------------------------------------------------------
*/
/*
* Permissions
- We create the permissions for the Supabase MakerKit. These permissions are used to manage the permissions for the roles
- The permissions are 'roles.manage', 'billing.manage', 'settings.manage', 'members.manage', and 'invites.manage'.
- You can add more permissions as needed.
*/
create type public.app_permissions as enum(
'roles.manage',
'billing.manage',
'settings.manage',
'members.manage',
'invites.manage'
);
/*
* Subscription Status
- We create the subscription status for the Supabase MakerKit. These statuses are used to manage the status of the subscriptions
- The statuses are 'active', 'trialing', 'past_due', 'canceled', 'unpaid', 'incomplete', 'incomplete_expired', and 'paused'.
- You can add more statuses as needed.
*/
create type public.subscription_status as ENUM(
'active',
'trialing',
'past_due',
'canceled',
'unpaid',
'incomplete',
'incomplete_expired',
'paused'
);
/*
Payment Status
- We create the payment status for the Supabase MakerKit. These statuses are used to manage the status of the payments
*/
create type public.payment_status as ENUM('pending', 'succeeded', 'failed');
/*
* Billing Provider
- We create the billing provider for the Supabase MakerKit. These providers are used to manage the billing provider for the accounts
- The providers are 'stripe', 'lemon-squeezy', and 'paddle'.
- You can add more providers as needed.
*/
create type public.billing_provider as ENUM('stripe', 'lemon-squeezy', 'paddle');
/*
* Subscription Item Type
- We create the subscription item type for the Supabase MakerKit. These types are used to manage the type of the subscription items
- The types are 'flat', 'per_seat', and 'metered'.
- You can add more types as needed.
*/
create type public.subscription_item_type as ENUM('flat', 'per_seat', 'metered');
/*
* Invitation Type
- We create the invitation type for the Supabase MakerKit. These types are used to manage the type of the invitation
*/
create type public.invitation as (email text, role varchar(50));

View File

@@ -1,145 +0,0 @@
/*
* -------------------------------------------------------
* Section: App Configuration
* We create the configuration for the Supabase MakerKit to enable or disable features
* -------------------------------------------------------
*/
create table if not exists
public.config (
enable_team_accounts boolean default true not null,
enable_account_billing boolean default true not null,
enable_team_account_billing boolean default true not null,
billing_provider public.billing_provider default 'stripe' not null
);
comment on table public.config is 'Configuration for the Supabase MakerKit.';
comment on column public.config.enable_team_accounts is 'Enable team accounts';
comment on column public.config.enable_account_billing is 'Enable billing for individual accounts';
comment on column public.config.enable_team_account_billing is 'Enable billing for team accounts';
comment on column public.config.billing_provider is 'The billing provider to use';
-- RLS(config)
alter table public.config enable row level security;
-- create config row
insert into
public.config (
enable_team_accounts,
enable_account_billing,
enable_team_account_billing
)
values
(true, true, true);
-- Revoke all on accounts table from authenticated and service_role
revoke all on public.config
from
authenticated,
service_role;
-- Open up access to config table for authenticated users and service_role
grant
select
on public.config to authenticated,
service_role;
-- RLS
-- SELECT(config):
-- Authenticated users can read the config
create policy "public config can be read by authenticated users" on public.config for
select
to authenticated using (true);
-- Function to get the config settings
create
or replace function public.get_config () returns json
set
search_path = '' as $$
declare
result record;
begin
select
*
from
public.config
limit 1 into result;
return row_to_json(result);
end;
$$ language plpgsql;
-- Automatically set timestamps on tables when a row is inserted or updated
create
or replace function public.trigger_set_timestamps () returns trigger
set
search_path = '' as $$
begin
if TG_OP = 'INSERT' then
new.created_at = now();
new.updated_at = now();
else
new.updated_at = now();
new.created_at = old.created_at;
end if;
return NEW;
end
$$ language plpgsql;
-- Automatically set user tracking on tables when a row is inserted or updated
create
or replace function public.trigger_set_user_tracking () returns trigger
set
search_path = '' as $$
begin
if TG_OP = 'INSERT' then
new.created_by = auth.uid();
new.updated_by = auth.uid();
else
new.updated_by = auth.uid();
new.created_by = old.created_by;
end if;
return NEW;
end
$$ language plpgsql;
grant
execute on function public.get_config () to authenticated,
service_role;
-- Function "public.is_set"
-- Check if a field is set in the config
create
or replace function public.is_set (field_name text) returns boolean
set
search_path = '' as $$
declare
result boolean;
begin
execute format('select %I from public.config limit 1', field_name) into result;
return result;
end;
$$ language plpgsql;
grant
execute on function public.is_set (text) to authenticated;

View File

@@ -1,560 +0,0 @@
/*
* -------------------------------------------------------
* Section: Accounts
* We create the schema for the accounts. Accounts are the top level entity in the Supabase MakerKit. They can be team or personal accounts.
* -------------------------------------------------------
*/
-- Accounts table
create table if not exists
public.accounts (
id uuid unique not null default extensions.uuid_generate_v4 (),
primary_owner_user_id uuid references auth.users on delete cascade not null default auth.uid (),
name varchar(255) not null,
slug text unique,
email varchar(320) unique,
is_personal_account boolean default false not null,
updated_at timestamp with time zone,
created_at timestamp with time zone,
created_by uuid references auth.users,
updated_by uuid references auth.users,
picture_url varchar(1000),
public_data jsonb default '{}'::jsonb not null,
primary key (id)
);
comment on table public.accounts is 'Accounts are the top level entity in the Supabase MakerKit. They can be team or personal accounts.';
comment on column public.accounts.is_personal_account is 'Whether the account is a personal account or not';
comment on column public.accounts.name is 'The name of the account';
comment on column public.accounts.slug is 'The slug of the account';
comment on column public.accounts.primary_owner_user_id is 'The primary owner of the account';
comment on column public.accounts.email is 'The email of the account. For teams, this is the email of the team (if any)';
-- Enable RLS on the accounts table
alter table "public"."accounts" enable row level security;
-- Revoke all on accounts table from authenticated and service_role
revoke all on public.accounts
from
authenticated,
service_role;
-- Open up access to accounts
grant
select
,
insert,
update,
delete on table public.accounts to authenticated,
service_role;
-- constraint that conditionally allows nulls on the slug ONLY if
-- personal_account is true
alter table public.accounts
add constraint accounts_slug_null_if_personal_account_true check (
(
is_personal_account = true
and slug is null
)
or (
is_personal_account = false
and slug is not null
)
);
-- Indexes
create index if not exists ix_accounts_primary_owner_user_id on public.accounts (primary_owner_user_id);
create index if not exists ix_accounts_is_personal_account on public.accounts (is_personal_account);
-- constraint to ensure that the primary_owner_user_id is unique for personal accounts
create unique index unique_personal_account on public.accounts (primary_owner_user_id)
where
is_personal_account = true;
-- RLS on the accounts table
-- UPDATE(accounts):
-- Team owners can update their accounts
create policy accounts_self_update on public.accounts
for update
to authenticated using (
(
select
auth.uid ()
) = primary_owner_user_id
)
with
check (
(
select
auth.uid ()
) = primary_owner_user_id
);
-- Function "public.transfer_team_account_ownership"
-- Function to transfer the ownership of a team account to another user
create
or replace function public.transfer_team_account_ownership (target_account_id uuid, new_owner_id uuid) returns void
set
search_path = '' as $$
begin
if current_user not in('service_role') then
raise exception 'You do not have permission to transfer account ownership';
end if;
-- verify the user is already a member of the account
if not exists(
select
1
from
public.accounts_memberships
where
target_account_id = account_id
and user_id = new_owner_id) then
raise exception 'The new owner must be a member of the account';
end if;
-- update the primary owner of the account
update
public.accounts
set
primary_owner_user_id = new_owner_id
where
id = target_account_id
and is_personal_account = false;
-- update membership assigning it the hierarchy role
update
public.accounts_memberships
set
account_role =(
public.get_upper_system_role())
where
target_account_id = account_id
and user_id = new_owner_id
and account_role <>(
public.get_upper_system_role());
end;
$$ language plpgsql;
grant
execute on function public.transfer_team_account_ownership (uuid, uuid) to service_role;
-- Function "public.is_account_owner"
-- Function to check if a user is the primary owner of an account
create
or replace function public.is_account_owner (account_id uuid) returns boolean
set
search_path = '' as $$
select
exists(
select
1
from
public.accounts
where
id = is_account_owner.account_id
and primary_owner_user_id = auth.uid());
$$ language sql;
grant
execute on function public.is_account_owner (uuid) to authenticated,
service_role;
-- Function "kit.protect_account_fields"
-- Function to protect account fields from being updated
create
or replace function kit.protect_account_fields () returns trigger as $$
begin
if current_user in('authenticated', 'anon') then
if new.id <> old.id or new.is_personal_account <>
old.is_personal_account or new.primary_owner_user_id <>
old.primary_owner_user_id or new.email <> old.email then
raise exception 'You do not have permission to update this field';
end if;
end if;
return NEW;
end
$$ language plpgsql
set
search_path = '';
-- trigger to protect account fields
create trigger protect_account_fields before
update on public.accounts for each row
execute function kit.protect_account_fields ();
-- Function "public.get_upper_system_role"
-- Function to get the highest system role for an account
create
or replace function public.get_upper_system_role () returns varchar
set
search_path = '' as $$
declare
role varchar(50);
begin
select name from public.roles
where hierarchy_level = 1 into role;
return role;
end;
$$ language plpgsql;
grant
execute on function public.get_upper_system_role () to service_role;
-- Function "kit.add_current_user_to_new_account"
-- Trigger to add the current user to a new account as the primary owner
create
or replace function kit.add_current_user_to_new_account () returns trigger language plpgsql security definer
set
search_path = '' as $$
begin
if new.primary_owner_user_id = auth.uid() then
insert into public.accounts_memberships(
account_id,
user_id,
account_role)
values(
new.id,
auth.uid(),
public.get_upper_system_role());
end if;
return NEW;
end;
$$;
-- trigger the function whenever a new account is created
create trigger "add_current_user_to_new_account"
after insert on public.accounts for each row
when (new.is_personal_account = false)
execute function kit.add_current_user_to_new_account ();
-- create a trigger to update the account email when the primary owner email is updated
create
or replace function kit.handle_update_user_email () returns trigger language plpgsql security definer
set
search_path = '' as $$
begin
update
public.accounts
set
email = new.email
where
primary_owner_user_id = new.id
and is_personal_account = true;
return new;
end;
$$;
-- trigger the function every time a user email is updated only if the user is the primary owner of the account and
-- the account is personal account
create trigger "on_auth_user_updated"
after
update of email on auth.users for each row
execute procedure kit.handle_update_user_email ();
/**
* -------------------------------------------------------
* Section: Slugify
* We create the schema for the slugify functions. Slugify functions are used to create slugs from strings.
* We use this for ensure unique slugs for accounts.
* -------------------------------------------------------
*/
-- Create a function to slugify a string
-- useful for turning an account name into a unique slug
create
or replace function kit.slugify ("value" text) returns text as $$
-- removes accents (diacritic signs) from a given string --
with "unaccented" as(
select
kit.unaccent("value") as "value"
),
-- lowercases the string
"lowercase" as(
select
lower("value") as "value"
from
"unaccented"
),
-- remove single and double quotes
"removed_quotes" as(
select
regexp_replace("value", '[''"]+', '',
'gi') as "value"
from
"lowercase"
),
-- replaces anything that's not a letter, number, hyphen('-'), or underscore('_') with a hyphen('-')
"hyphenated" as(
select
regexp_replace("value", '[^a-z0-9\\-_]+', '-',
'gi') as "value"
from
"removed_quotes"
),
-- trims hyphens('-') if they exist on the head or tail of
-- the string
"trimmed" as(
select
regexp_replace(regexp_replace("value", '\-+$',
''), '^\-', '') as "value" from "hyphenated"
)
select
"value"
from
"trimmed";
$$ language SQL strict immutable
set
search_path to '';
grant
execute on function kit.slugify (text) to service_role,
authenticated;
-- Function "kit.set_slug_from_account_name"
-- Set the slug from the account name and increment if the slug exists
create
or replace function kit.set_slug_from_account_name () returns trigger language plpgsql security definer
set
search_path = '' as $$
declare
sql_string varchar;
tmp_slug varchar;
increment integer;
tmp_row record;
tmp_row_count integer;
begin
tmp_row_count = 1;
increment = 0;
while tmp_row_count > 0 loop
if increment > 0 then
tmp_slug = kit.slugify(new.name || ' ' || increment::varchar);
else
tmp_slug = kit.slugify(new.name);
end if;
sql_string = format('select count(1) cnt from public.accounts where slug = ''' || tmp_slug ||
'''; ');
for tmp_row in execute (sql_string)
loop
raise notice 'tmp_row %', tmp_row;
tmp_row_count = tmp_row.cnt;
end loop;
increment = increment +1;
end loop;
new.slug := tmp_slug;
return NEW;
end
$$;
-- Create a trigger to set the slug from the account name
create trigger "set_slug_from_account_name" before insert on public.accounts for each row when (
NEW.name is not null
and NEW.slug is null
and NEW.is_personal_account = false
)
execute procedure kit.set_slug_from_account_name ();
-- Create a trigger when a name is updated to update the slug
create trigger "update_slug_from_account_name" before
update on public.accounts for each row when (
NEW.name is not null
and NEW.name <> OLD.name
and NEW.is_personal_account = false
)
execute procedure kit.set_slug_from_account_name ();
-- Function "kit.setup_new_user"
-- Setup a new user account after user creation
create
or replace function kit.setup_new_user () returns trigger language plpgsql security definer
set
search_path = '' as $$
declare
user_name text;
picture_url text;
begin
if new.raw_user_meta_data ->> 'name' is not null then
user_name := new.raw_user_meta_data ->> 'name';
end if;
if user_name is null and new.email is not null then
user_name := split_part(new.email, '@', 1);
end if;
if user_name is null then
user_name := '';
end if;
if new.raw_user_meta_data ->> 'avatar_url' is not null then
picture_url := new.raw_user_meta_data ->> 'avatar_url';
else
picture_url := null;
end if;
insert into public.accounts(
id,
primary_owner_user_id,
name,
is_personal_account,
picture_url,
email)
values (
new.id,
new.id,
user_name,
true,
picture_url,
new.email);
return new;
end;
$$;
-- trigger the function every time a user is created
create trigger on_auth_user_created
after insert on auth.users for each row
execute procedure kit.setup_new_user ();
/**
* -------------------------------------------------------
* Section: Functions
* We create the schema for the functions
* -------------------------------------------------------
*/
-- Function "public.create_team_account"
-- Create a team account if team accounts are enabled
create
or replace function public.create_team_account (account_name text) returns public.accounts
set
search_path = '' as $$
declare
new_account public.accounts;
begin
if (not public.is_set('enable_team_accounts')) then
raise exception 'Team accounts are not enabled';
end if;
insert into public.accounts(
name,
is_personal_account)
values (
account_name,
false)
returning
* into new_account;
return new_account;
end;
$$ language plpgsql;
grant
execute on function public.create_team_account (text) to authenticated,
service_role;
-- RLS(public.accounts)
-- Authenticated users can create team accounts
create policy create_org_account on public.accounts for insert to authenticated
with
check (
public.is_set ('enable_team_accounts')
and public.accounts.is_personal_account = false
);
-- RLS(public.accounts)
-- Authenticated users can delete team accounts
create policy delete_team_account
on public.accounts
for delete
to authenticated
using (
auth.uid() = primary_owner_user_id
);
-- Functions "public.get_account_members"
-- Function to get the members of an account by the account slug
create
or replace function public.get_account_members (account_slug text) returns table (
id uuid,
user_id uuid,
account_id uuid,
role varchar(50),
role_hierarchy_level int,
primary_owner_user_id uuid,
name varchar,
email varchar,
picture_url varchar,
created_at timestamptz,
updated_at timestamptz
) language plpgsql
set
search_path = '' as $$
begin
return QUERY
select
acc.id,
am.user_id,
am.account_id,
am.account_role,
r.hierarchy_level,
a.primary_owner_user_id,
acc.name,
acc.email,
acc.picture_url,
am.created_at,
am.updated_at
from
public.accounts_memberships am
join public.accounts a on a.id = am.account_id
join public.accounts acc on acc.id = am.user_id
join public.roles r on r.name = am.account_role
where
a.slug = account_slug;
end;
$$;
grant
execute on function public.get_account_members (text) to authenticated,
service_role;

View File

@@ -1,30 +0,0 @@
/*
* -------------------------------------------------------
* Section: Roles
* We create the schema for the roles. Roles are the roles for an account. For example, an account might have the roles 'owner', 'admin', and 'member'.
* -------------------------------------------------------
*/
-- Roles Table
create table if not exists
public.roles (
name varchar(50) not null,
hierarchy_level int not null check (hierarchy_level > 0),
primary key (name),
unique (hierarchy_level)
);
-- Revoke all on roles table from authenticated and service_role
revoke all on public.roles
from
authenticated,
service_role;
-- Open up access to roles table for authenticated users and service_role
grant
select
on table public.roles to authenticated,
service_role;
-- RLS
alter table public.roles enable row level security;

View File

@@ -1,312 +0,0 @@
/*
* -------------------------------------------------------
* Section: Memberships
* We create the schema for the memberships. Memberships are the memberships for an account. For example, a user might be a member of an account with the role 'owner'.
* -------------------------------------------------------
*/
-- Account Memberships table
create table if not exists
public.accounts_memberships (
user_id uuid references auth.users on delete cascade not null,
account_id uuid references public.accounts (id) on delete cascade not null,
account_role varchar(50) references public.roles (name) not null,
created_at timestamptz default current_timestamp not null,
updated_at timestamptz default current_timestamp not null,
created_by uuid references auth.users,
updated_by uuid references auth.users,
primary key (user_id, account_id)
);
comment on table public.accounts_memberships is 'The memberships for an account';
comment on column public.accounts_memberships.account_id is 'The account the membership is for';
comment on column public.accounts_memberships.account_role is 'The role for the membership';
-- Revoke all on accounts_memberships table from authenticated and service_role
revoke all on public.accounts_memberships
from
authenticated,
service_role;
-- Open up access to accounts_memberships table for authenticated users and service_role
grant
select
,
insert,
update,
delete on table public.accounts_memberships to authenticated,
service_role;
-- Indexes on the accounts_memberships table
create index ix_accounts_memberships_account_id on public.accounts_memberships (account_id);
create index ix_accounts_memberships_user_id on public.accounts_memberships (user_id);
create index ix_accounts_memberships_account_role on public.accounts_memberships (account_role);
-- Enable RLS on the accounts_memberships table
alter table public.accounts_memberships enable row level security;
-- Function "kit.prevent_account_owner_membership_delete"
-- Trigger to prevent a primary owner from being removed from an account
create
or replace function kit.prevent_account_owner_membership_delete () returns trigger
set
search_path = '' as $$
begin
if exists(
select
1
from
public.accounts
where
id = old.account_id
and primary_owner_user_id = old.user_id) then
raise exception 'The primary account owner cannot be removed from the account membership list';
end if;
return old;
end;
$$ language plpgsql;
create
or replace trigger prevent_account_owner_membership_delete_check before delete on public.accounts_memberships for each row
execute function kit.prevent_account_owner_membership_delete ();
-- Function "kit.prevent_memberships_update"
-- Trigger to prevent updates to account memberships with the exception of the account_role
create
or replace function kit.prevent_memberships_update () returns trigger
set
search_path = '' as $$
begin
if new.account_role <> old.account_role then
return new;
end if;
raise exception 'Only the account_role can be updated';
end; $$ language plpgsql;
create
or replace trigger prevent_memberships_update_check before
update on public.accounts_memberships for each row
execute function kit.prevent_memberships_update ();
-- Function "public.has_role_on_account"
-- Function to check if a user has a role on an account
create
or replace function public.has_role_on_account (
account_id uuid,
account_role varchar(50) default null
) returns boolean language sql security definer
set
search_path = '' as $$
select
exists(
select
1
from
public.accounts_memberships membership
where
membership.user_id = (select auth.uid())
and membership.account_id = has_role_on_account.account_id
and((membership.account_role = has_role_on_account.account_role
or has_role_on_account.account_role is null)));
$$;
grant
execute on function public.has_role_on_account (uuid, varchar) to authenticated;
-- Function "public.is_team_member"
-- Check if a user is a team member of an account or not
create
or replace function public.is_team_member (account_id uuid, user_id uuid) returns boolean language sql security definer
set
search_path = '' as $$
select
exists(
select
1
from
public.accounts_memberships membership
where
public.has_role_on_account(account_id)
and membership.user_id = is_team_member.user_id
and membership.account_id = is_team_member.account_id);
$$;
grant
execute on function public.is_team_member (uuid, uuid) to authenticated,
service_role;
-- RLS
-- SELECT(roles)
-- authenticated users can query roles
create policy roles_read on public.roles for
select
to authenticated using (
true
);
-- Function "public.can_action_account_member"
-- Check if a user can perform management actions on an account member
create
or replace function public.can_action_account_member (target_team_account_id uuid, target_user_id uuid) returns boolean
set
search_path = '' as $$
declare
permission_granted boolean;
target_user_hierarchy_level int;
current_user_hierarchy_level int;
is_account_owner boolean;
target_user_role varchar(50);
begin
if target_user_id = auth.uid() then
raise exception 'You cannot update your own account membership with this function';
end if;
-- an account owner can action any member of the account
if public.is_account_owner(target_team_account_id) then
return true;
end if;
-- check the target user is the primary owner of the account
select
exists (
select
1
from
public.accounts
where
id = target_team_account_id
and primary_owner_user_id = target_user_id) into is_account_owner;
if is_account_owner then
raise exception 'The primary account owner cannot be actioned';
end if;
-- validate the auth user has the required permission on the account
-- to manage members of the account
select
public.has_permission(auth.uid(), target_team_account_id,
'members.manage'::public.app_permissions) into
permission_granted;
-- if the user does not have the required permission, raise an exception
if not permission_granted then
raise exception 'You do not have permission to action a member from this account';
end if;
-- get the role of the target user
select
am.account_role,
r.hierarchy_level
from
public.accounts_memberships as am
join
public.roles as r on am.account_role = r.name
where
am.account_id = target_team_account_id
and am.user_id = target_user_id
into target_user_role, target_user_hierarchy_level;
-- get the hierarchy level of the current user
select
r.hierarchy_level into current_user_hierarchy_level
from
public.roles as r
join
public.accounts_memberships as am on r.name = am.account_role
where
am.account_id = target_team_account_id
and am.user_id = auth.uid();
if target_user_role is null then
raise exception 'The target user does not have a role on the account';
end if;
if current_user_hierarchy_level is null then
raise exception 'The current user does not have a role on the account';
end if;
-- check the current user has a higher role than the target user
if current_user_hierarchy_level >= target_user_hierarchy_level then
raise exception 'You do not have permission to action a member from this account';
end if;
return true;
end;
$$ language plpgsql;
grant
execute on function public.can_action_account_member (uuid, uuid) to authenticated,
service_role;
-- RLS
-- SELECT(accounts_memberships):
-- Users can read their team members account memberships
create policy accounts_memberships_read on public.accounts_memberships for
select
to authenticated using (
(
(
select
auth.uid ()
) = user_id
)
or is_team_member (account_id, user_id)
);
create
or replace function public.is_account_team_member (target_account_id uuid) returns boolean
set
search_path = '' as $$
select exists(
select 1
from public.accounts_memberships as membership
where public.is_team_member (membership.account_id, target_account_id)
);
$$ language sql;
grant
execute on function public.is_account_team_member (uuid) to authenticated,
service_role;
-- RLS on the accounts table
-- SELECT(accounts):
-- Users can read the an account if
-- - they are the primary owner of the account
-- - they have a role on the account
-- - they are reading an account of the same team
create policy accounts_read on public.accounts for
select
to authenticated using (
(
(
select
auth.uid ()
) = primary_owner_user_id
)
or public.has_role_on_account (id)
or public.is_account_team_member (id)
);
-- DELETE(accounts_memberships):
-- Users with the required role can remove members from an account or remove their own
create policy accounts_memberships_delete on public.accounts_memberships for delete to authenticated using (
(
user_id = (
select
auth.uid ()
)
)
or public.can_action_account_member (account_id, user_id)
);

View File

@@ -1,237 +0,0 @@
/*
* -------------------------------------------------------
* Section: Role Permissions
* We create the schema for the role permissions. Role permissions are the permissions for a role.
* For example, the 'owner' role might have the 'roles.manage' permission.
* -------------------------------------------------------
*/
-- Create table for roles permissions
create table if not exists
public.role_permissions (
id bigint generated by default as identity primary key,
role varchar(50) references public.roles (name) not null,
permission public.app_permissions not null,
unique (role, permission)
);
comment on table public.role_permissions is 'The permissions for a role';
comment on column public.role_permissions.role is 'The role the permission is for';
comment on column public.role_permissions.permission is 'The permission for the role';
-- Indexes on the role_permissions table
create index ix_role_permissions_role on public.role_permissions (role);
-- Revoke all on role_permissions table from authenticated and service_role
revoke all on public.role_permissions
from
authenticated,
service_role;
-- Open up access to role_permissions table for authenticated users and service_role
grant
select
,
insert,
update,
delete on table public.role_permissions to service_role;
-- Authenticated users can read role permissions
grant
select
on table public.role_permissions to authenticated;
-- Function "public.has_permission"
-- Create a function to check if a user has a permission
create
or replace function public.has_permission (
user_id uuid,
account_id uuid,
permission_name public.app_permissions
) returns boolean
set
search_path = '' as $$
begin
return exists(
select
1
from
public.accounts_memberships
join public.role_permissions on
accounts_memberships.account_role =
role_permissions.role
where
accounts_memberships.user_id = has_permission.user_id
and accounts_memberships.account_id = has_permission.account_id
and role_permissions.permission = has_permission.permission_name);
end;
$$ language plpgsql;
grant
execute on function public.has_permission (uuid, uuid, public.app_permissions) to authenticated,
service_role;
-- Function "public.has_more_elevated_role"
-- Check if a user has a more elevated role than the target role
create
or replace function public.has_more_elevated_role (
target_user_id uuid,
target_account_id uuid,
role_name varchar
) returns boolean
set
search_path = '' as $$
declare
declare is_primary_owner boolean;
user_role_hierarchy_level int;
target_role_hierarchy_level int;
begin
-- Check if the user is the primary owner of the account
select
exists (
select
1
from
public.accounts
where
id = target_account_id
and primary_owner_user_id = target_user_id) into is_primary_owner;
-- If the user is the primary owner, they have the highest role and can
-- perform any action
if is_primary_owner then
return true;
end if;
-- Get the hierarchy level of the user's role within the account
select
hierarchy_level into user_role_hierarchy_level
from
public.roles
where
name =(
select
account_role
from
public.accounts_memberships
where
account_id = target_account_id
and target_user_id = user_id);
if user_role_hierarchy_level is null then
return false;
end if;
-- Get the hierarchy level of the target role
select
hierarchy_level into target_role_hierarchy_level
from
public.roles
where
name = role_name;
-- If the target role does not exist, the user cannot perform the action
if target_role_hierarchy_level is null then
return false;
end if;
-- If the user's role is higher than the target role, they can perform
-- the action
return user_role_hierarchy_level < target_role_hierarchy_level;
end;
$$ language plpgsql;
grant
execute on function public.has_more_elevated_role (uuid, uuid, varchar) to authenticated,
service_role;
-- Function "public.has_same_role_hierarchy_level"
-- Check if a user has the same role hierarchy level as the target role
create
or replace function public.has_same_role_hierarchy_level (
target_user_id uuid,
target_account_id uuid,
role_name varchar
) returns boolean
set
search_path = '' as $$
declare
is_primary_owner boolean;
user_role_hierarchy_level int;
target_role_hierarchy_level int;
begin
-- Check if the user is the primary owner of the account
select
exists (
select
1
from
public.accounts
where
id = target_account_id
and primary_owner_user_id = target_user_id) into is_primary_owner;
-- If the user is the primary owner, they have the highest role and can perform any action
if is_primary_owner then
return true;
end if;
-- Get the hierarchy level of the user's role within the account
select
hierarchy_level into user_role_hierarchy_level
from
public.roles
where
name =(
select
account_role
from
public.accounts_memberships
where
account_id = target_account_id
and target_user_id = user_id);
-- If the user does not have a role in the account, they cannot perform the action
if user_role_hierarchy_level is null then
return false;
end if;
-- Get the hierarchy level of the target role
select
hierarchy_level into target_role_hierarchy_level
from
public.roles
where
name = role_name;
-- If the target role does not exist, the user cannot perform the action
if target_role_hierarchy_level is null then
return false;
end if;
-- check the user's role hierarchy level is the same as the target role
return user_role_hierarchy_level = target_role_hierarchy_level;
end;
$$ language plpgsql;
grant
execute on function public.has_same_role_hierarchy_level (uuid, uuid, varchar) to authenticated,
service_role;
-- Enable RLS on the role_permissions table
alter table public.role_permissions enable row level security;
-- RLS on the role_permissions table
-- SELECT(role_permissions):
-- Authenticated Users can read global permissions
create policy role_permissions_read on public.role_permissions for
select
to authenticated using (true);

View File

@@ -1,354 +0,0 @@
/*
* -------------------------------------------------------
* Section: Invitations
* We create the schema for the invitations. Invitations are the invitations for an account sent to a user to join the account.
* -------------------------------------------------------
*/
create table if not exists
public.invitations (
id serial primary key,
email varchar(255) not null,
account_id uuid references public.accounts (id) on delete cascade not null,
invited_by uuid references auth.users on delete cascade not null,
role varchar(50) references public.roles (name) not null,
invite_token varchar(255) unique not null,
created_at timestamptz default current_timestamp not null,
updated_at timestamptz default current_timestamp not null,
expires_at timestamptz default current_timestamp + interval '7 days' not null,
unique (email, account_id)
);
comment on table public.invitations is 'The invitations for an account';
comment on column public.invitations.account_id is 'The account the invitation is for';
comment on column public.invitations.invited_by is 'The user who invited the user';
comment on column public.invitations.role is 'The role for the invitation';
comment on column public.invitations.invite_token is 'The token for the invitation';
comment on column public.invitations.expires_at is 'The expiry date for the invitation';
comment on column public.invitations.email is 'The email of the user being invited';
-- Indexes on the invitations table
create index ix_invitations_account_id on public.invitations (account_id);
-- Revoke all on invitations table from authenticated and service_role
revoke all on public.invitations
from
authenticated,
service_role;
-- Open up access to invitations table for authenticated users and service_role
grant
select
,
insert,
update,
delete on table public.invitations to authenticated,
service_role;
-- Enable RLS on the invitations table
alter table public.invitations enable row level security;
-- Function "kit.check_team_account"
-- Function to check if the account is a team account or not when inserting or updating an invitation
create
or replace function kit.check_team_account () returns trigger
set
search_path = '' as $$
begin
if(
select
is_personal_account
from
public.accounts
where
id = new.account_id) then
raise exception 'Account must be an team account';
end if;
return NEW;
end;
$$ language plpgsql;
create trigger only_team_accounts_check before insert
or
update on public.invitations for each row
execute procedure kit.check_team_account ();
-- RLS on the invitations table
-- SELECT(invitations):
-- Users can read invitations to users of an account they are a member of
create policy invitations_read_self on public.invitations for
select
to authenticated using (public.has_role_on_account (account_id));
-- INSERT(invitations):
-- Users can create invitations to users of an account they are
-- a member of and have the 'invites.manage' permission AND the target role is not higher than the user's role
create policy invitations_create_self on public.invitations for insert to authenticated
with
check (
public.is_set ('enable_team_accounts')
and public.has_permission (
(
select
auth.uid ()
),
account_id,
'invites.manage'::public.app_permissions
)
and (public.has_more_elevated_role (
(
select
auth.uid ()
),
account_id,
role
) or public.has_same_role_hierarchy_level(
(
select
auth.uid ()
),
account_id,
role
))
);
-- UPDATE(invitations):
-- Users can update invitations to users of an account they are a member of and have the 'invites.manage' permission AND
-- the target role is not higher than the user's role
create policy invitations_update on public.invitations
for update
to authenticated using (
public.has_permission (
(
select
auth.uid ()
),
account_id,
'invites.manage'::public.app_permissions
)
and public.has_more_elevated_role (
(
select
auth.uid ()
),
account_id,
role
)
)
with
check (
public.has_permission (
(
select
auth.uid ()
),
account_id,
'invites.manage'::public.app_permissions
)
and public.has_more_elevated_role (
(
select
auth.uid ()
),
account_id,
role
)
);
-- DELETE(public.invitations):
-- Users can delete invitations to users of an account they are a member of and have the 'invites.manage' permission
create policy invitations_delete on public.invitations for delete to authenticated using (
has_role_on_account (account_id)
and public.has_permission (
(
select
auth.uid ()
),
account_id,
'invites.manage'::public.app_permissions
)
);
-- Functions "public.accept_invitation"
-- Function to accept an invitation to an account
create
or replace function accept_invitation (token text, user_id uuid) returns uuid
set
search_path = '' as $$
declare
target_account_id uuid;
target_role varchar(50);
begin
select
account_id,
role into target_account_id,
target_role
from
public.invitations
where
invite_token = token
and expires_at > now();
if not found then
raise exception 'Invalid or expired invitation token';
end if;
insert into public.accounts_memberships(
user_id,
account_id,
account_role)
values (
accept_invitation.user_id,
target_account_id,
target_role);
delete from public.invitations
where invite_token = token;
return target_account_id;
end;
$$ language plpgsql;
grant
execute on function accept_invitation (text, uuid) to service_role;
-- Function "public.create_invitation"
-- create an invitation to an account
create
or replace function public.create_invitation (account_id uuid, email text, role varchar(50)) returns public.invitations
set
search_path = '' as $$
declare
new_invitation public.invitations;
invite_token text;
begin
invite_token := extensions.uuid_generate_v4();
insert into public.invitations(
email,
account_id,
invited_by,
role,
invite_token)
values (
email,
account_id,
auth.uid(),
role,
invite_token)
returning
* into new_invitation;
return new_invitation;
end;
$$ language plpgsql;
-- Function "public.get_account_invitations"
-- List the account invitations by the account slug
create
or replace function public.get_account_invitations (account_slug text) returns table (
id integer,
email varchar(255),
account_id uuid,
invited_by uuid,
role varchar(50),
created_at timestamptz,
updated_at timestamptz,
expires_at timestamptz,
inviter_name varchar,
inviter_email varchar
)
set
search_path = '' as $$
begin
return query
select
invitation.id,
invitation.email,
invitation.account_id,
invitation.invited_by,
invitation.role,
invitation.created_at,
invitation.updated_at,
invitation.expires_at,
account.name,
account.email
from
public.invitations as invitation
join public.accounts as account on invitation.account_id = account.id
where
account.slug = account_slug;
end;
$$ language plpgsql;
grant
execute on function public.get_account_invitations (text) to authenticated,
service_role;
-- Function "public.add_invitations_to_account"
-- Add invitations to an account
create
or replace function public.add_invitations_to_account (
account_slug text,
invitations public.invitation[]
) returns public.invitations[]
set
search_path = '' as $$
declare
new_invitation public.invitations;
all_invitations public.invitations[] := array[]::public.invitations[];
invite_token text;
email text;
role varchar(50);
begin
FOREACH email,
role in array invitations loop
invite_token := extensions.uuid_generate_v4();
insert into public.invitations(
email,
account_id,
invited_by,
role,
invite_token)
values (
email,
(
select
id
from
public.accounts
where
slug = account_slug), auth.uid(), role, invite_token)
returning
* into new_invitation;
all_invitations := array_append(all_invitations, new_invitation);
end loop;
return all_invitations;
end;
$$ language plpgsql;
grant
execute on function public.add_invitations_to_account (text, public.invitation[]) to authenticated,
service_role;

View File

@@ -1,66 +0,0 @@
/*
* -------------------------------------------------------
* Section: Billing Customers
* We create the schema for the billing customers. Billing customers are the customers for an account in the billing provider. For example, a user might have a customer in the billing provider with the customer ID 'cus_123'.
* -------------------------------------------------------
*/
-- Billing Customers table
create table
public.billing_customers (
account_id uuid references public.accounts (id) on delete cascade not null,
id serial primary key,
email text,
provider public.billing_provider not null,
customer_id text not null,
unique (account_id, customer_id, provider)
);
comment on table public.billing_customers is 'The billing customers for an account';
comment on column public.billing_customers.account_id is 'The account the billing customer is for';
comment on column public.billing_customers.provider is 'The provider of the billing customer';
comment on column public.billing_customers.customer_id is 'The customer ID for the billing customer';
comment on column public.billing_customers.email is 'The email of the billing customer';
-- Indexes on the billing_customers table
create index ix_billing_customers_account_id on public.billing_customers (account_id);
-- Revoke all on billing_customers table from authenticated and service_role
revoke all on public.billing_customers
from
authenticated,
service_role;
-- Open up relevant access to billing_customers table for authenticated users and service_role
grant
select
,
insert,
update,
delete on table public.billing_customers to service_role;
-- Open up access to billing_customers table for authenticated users
grant
select
on table public.billing_customers to authenticated,
service_role;
-- Enable RLS on billing_customers table
alter table public.billing_customers enable row level security;
-- RLS on the billing_customers table
-- SELECT(billing_customers):
-- Users can read account subscriptions on an account they are a member of
create policy billing_customers_read_self on public.billing_customers for
select
to authenticated using (
account_id = (
select
auth.uid ()
)
or has_role_on_account (account_id)
);

View File

@@ -1,366 +0,0 @@
/*
* -------------------------------------------------------
* Section: Subscriptions
* We create the schema for the subscriptions. Subscriptions are the subscriptions for an account to a product. For example, a user might have a subscription to a product with the status 'active'.
* -------------------------------------------------------
*/
-- Subscriptions table
create table if not exists
public.subscriptions (
id text not null primary key,
account_id uuid references public.accounts (id) on delete cascade not null,
billing_customer_id int references public.billing_customers on delete cascade not null,
status public.subscription_status not null,
active bool not null,
billing_provider public.billing_provider not null,
cancel_at_period_end bool not null,
currency varchar(3) not null,
created_at timestamptz not null default current_timestamp,
updated_at timestamptz not null default current_timestamp,
period_starts_at timestamptz not null,
period_ends_at timestamptz not null,
trial_starts_at timestamptz,
trial_ends_at timestamptz
);
comment on table public.subscriptions is 'The subscriptions for an account';
comment on column public.subscriptions.account_id is 'The account the subscription is for';
comment on column public.subscriptions.billing_provider is 'The provider of the subscription';
comment on column public.subscriptions.cancel_at_period_end is 'Whether the subscription will be canceled at the end of the period';
comment on column public.subscriptions.currency is 'The currency for the subscription';
comment on column public.subscriptions.status is 'The status of the subscription';
comment on column public.subscriptions.period_starts_at is 'The start of the current period for the subscription';
comment on column public.subscriptions.period_ends_at is 'The end of the current period for the subscription';
comment on column public.subscriptions.trial_starts_at is 'The start of the trial period for the subscription';
comment on column public.subscriptions.trial_ends_at is 'The end of the trial period for the subscription';
comment on column public.subscriptions.active is 'Whether the subscription is active';
comment on column public.subscriptions.billing_customer_id is 'The billing customer ID for the subscription';
-- Revoke all on subscriptions table from authenticated and service_role
revoke all on public.subscriptions
from
authenticated,
service_role;
-- Open up relevant access to subscriptions table for authenticated users and service_role
grant
select
,
insert,
update,
delete on table public.subscriptions to service_role;
grant
select
on table public.subscriptions to authenticated;
-- Indexes on the subscriptions table
create index ix_subscriptions_account_id on public.subscriptions (account_id);
-- Enable RLS on subscriptions table
alter table public.subscriptions enable row level security;
-- RLS on the subscriptions table
-- SELECT(subscriptions):
-- Users can read account subscriptions on an account they are a member of
create policy subscriptions_read_self on public.subscriptions for
select
to authenticated using (
(
has_role_on_account (account_id)
and public.is_set ('enable_team_account_billing')
)
or (
account_id = (
select
auth.uid ()
)
and public.is_set ('enable_account_billing')
)
);
-- Function "public.upsert_subscription"
-- Insert or Update a subscription and its items in the database when receiving a webhook from the billing provider
create
or replace function public.upsert_subscription (
target_account_id uuid,
target_customer_id varchar(255),
target_subscription_id text,
active bool,
status public.subscription_status,
billing_provider public.billing_provider,
cancel_at_period_end bool,
currency varchar(3),
period_starts_at timestamptz,
period_ends_at timestamptz,
line_items jsonb,
trial_starts_at timestamptz default null,
trial_ends_at timestamptz default null
) returns public.subscriptions
set
search_path = '' as $$
declare
new_subscription public.subscriptions;
new_billing_customer_id int;
begin
insert into public.billing_customers(
account_id,
provider,
customer_id)
values (
target_account_id,
billing_provider,
target_customer_id)
on conflict (
account_id,
provider,
customer_id)
do update set
provider = excluded.provider
returning
id into new_billing_customer_id;
insert into public.subscriptions(
account_id,
billing_customer_id,
id,
active,
status,
billing_provider,
cancel_at_period_end,
currency,
period_starts_at,
period_ends_at,
trial_starts_at,
trial_ends_at)
values (
target_account_id,
new_billing_customer_id,
target_subscription_id,
active,
status,
billing_provider,
cancel_at_period_end,
currency,
period_starts_at,
period_ends_at,
trial_starts_at,
trial_ends_at)
on conflict (
id)
do update set
active = excluded.active,
status = excluded.status,
cancel_at_period_end = excluded.cancel_at_period_end,
currency = excluded.currency,
period_starts_at = excluded.period_starts_at,
period_ends_at = excluded.period_ends_at,
trial_starts_at = excluded.trial_starts_at,
trial_ends_at = excluded.trial_ends_at
returning
* into new_subscription;
-- Upsert subscription items and delete ones that are not in the line_items array
with item_data as (
select
(line_item ->> 'id')::varchar as line_item_id,
(line_item ->> 'product_id')::varchar as prod_id,
(line_item ->> 'variant_id')::varchar as var_id,
(line_item ->> 'type')::public.subscription_item_type as type,
(line_item ->> 'price_amount')::numeric as price_amt,
(line_item ->> 'quantity')::integer as qty,
(line_item ->> 'interval')::varchar as intv,
(line_item ->> 'interval_count')::integer as intv_count
from
jsonb_array_elements(line_items) as line_item
),
line_item_ids as (
select line_item_id from item_data
),
deleted_items as (
delete from
public.subscription_items
where
public.subscription_items.subscription_id = new_subscription.id
and public.subscription_items.id not in (select line_item_id from line_item_ids)
returning *
)
insert into public.subscription_items(
id,
subscription_id,
product_id,
variant_id,
type,
price_amount,
quantity,
interval,
interval_count)
select
line_item_id,
target_subscription_id,
prod_id,
var_id,
type,
price_amt,
qty,
intv,
intv_count
from
item_data
on conflict (id)
do update set
product_id = excluded.product_id,
variant_id = excluded.variant_id,
price_amount = excluded.price_amount,
quantity = excluded.quantity,
interval = excluded.interval,
type = excluded.type,
interval_count = excluded.interval_count;
return new_subscription;
end;
$$ language plpgsql;
grant
execute on function public.upsert_subscription (
uuid,
varchar,
text,
bool,
public.subscription_status,
public.billing_provider,
bool,
varchar,
timestamptz,
timestamptz,
jsonb,
timestamptz,
timestamptz
) to service_role;
/* -------------------------------------------------------
* Section: Subscription Items
* We create the schema for the subscription items. Subscription items are the items in a subscription.
* For example, a subscription might have a subscription item with the product ID 'prod_123' and the variant ID 'var_123'.
* -------------------------------------------------------
*/
create table if not exists
public.subscription_items (
id varchar(255) not null primary key,
subscription_id text references public.subscriptions (id) on delete cascade not null,
product_id varchar(255) not null,
variant_id varchar(255) not null,
type public.subscription_item_type not null,
price_amount numeric,
quantity integer not null default 1,
interval varchar(255) not null,
interval_count integer not null check (interval_count > 0),
created_at timestamptz not null default current_timestamp,
updated_at timestamptz not null default current_timestamp,
unique (subscription_id, product_id, variant_id)
);
comment on table public.subscription_items is 'The items in a subscription';
comment on column public.subscription_items.subscription_id is 'The subscription the item is for';
comment on column public.subscription_items.product_id is 'The product ID for the item';
comment on column public.subscription_items.variant_id is 'The variant ID for the item';
comment on column public.subscription_items.price_amount is 'The price amount for the item';
comment on column public.subscription_items.quantity is 'The quantity of the item';
comment on column public.subscription_items.interval is 'The interval for the item';
comment on column public.subscription_items.interval_count is 'The interval count for the item';
comment on column public.subscription_items.created_at is 'The creation date of the item';
comment on column public.subscription_items.updated_at is 'The last update date of the item';
-- Revoke all access to subscription_items table for authenticated users and service_role
revoke all on public.subscription_items
from
authenticated,
service_role;
-- Open up relevant access to subscription_items table for authenticated users and service_role
grant
select
on table public.subscription_items to authenticated,
service_role;
grant insert,
update,
delete on table public.subscription_items to service_role;
-- Indexes
-- Indexes on the subscription_items table
create index ix_subscription_items_subscription_id on public.subscription_items (subscription_id);
-- RLS
alter table public.subscription_items enable row level security;
-- SELECT(subscription_items)
-- Users can read subscription items on a subscription they are a member of
create policy subscription_items_read_self on public.subscription_items for
select
to authenticated using (
exists (
select
1
from
public.subscriptions
where
id = subscription_id
and (
account_id = (
select
auth.uid ()
)
or has_role_on_account (account_id)
)
)
);
-- Function "public.has_active_subscription"
-- Check if a user has an active subscription on an account - ie. it's trialing or active
-- Useful to gate access to features that require a subscription
create
or replace function public.has_active_subscription (target_account_id uuid) returns boolean
set
search_path = '' as $$
begin
return exists (
select
1
from
public.subscriptions
where
account_id = target_account_id
and active = true);
end;
$$ language plpgsql;
grant
execute on function public.has_active_subscription (uuid) to authenticated,
service_role;

View File

@@ -1,280 +0,0 @@
/**
* -------------------------------------------------------
* Section: Orders
* We create the schema for the subscription items. Subscription items are the items in a subscription.
* For example, a subscription might have a subscription item with the product ID 'prod_123' and the variant ID 'var_123'.
* -------------------------------------------------------
*/
create table if not exists
public.orders (
id text not null primary key,
account_id uuid references public.accounts (id) on delete cascade not null,
billing_customer_id int references public.billing_customers on delete cascade not null,
status public.payment_status not null,
billing_provider public.billing_provider not null,
total_amount numeric not null,
currency varchar(3) not null,
created_at timestamptz not null default current_timestamp,
updated_at timestamptz not null default current_timestamp
);
comment on table public.orders is 'The one-time orders for an account';
comment on column public.orders.account_id is 'The account the order is for';
comment on column public.orders.billing_provider is 'The provider of the order';
comment on column public.orders.total_amount is 'The total amount for the order';
comment on column public.orders.currency is 'The currency for the order';
comment on column public.orders.status is 'The status of the order';
comment on column public.orders.billing_customer_id is 'The billing customer ID for the order';
-- Revoke all access to orders table for authenticated users and service_role
revoke all on public.orders
from
authenticated,
service_role;
-- Open up access to orders table for authenticated users and service_role
grant
select
on table public.orders to authenticated;
grant
select
,
insert,
update,
delete on table public.orders to service_role;
-- Indexes
-- Indexes on the orders table
create index ix_orders_account_id on public.orders (account_id);
-- RLS
alter table public.orders enable row level security;
-- SELECT(orders)
-- Users can read orders on an account they are a member of or the account is their own
create policy orders_read_self on public.orders for
select
to authenticated using (
(
account_id = (
select
auth.uid ()
)
and public.is_set ('enable_account_billing')
)
or (
has_role_on_account (account_id)
and public.is_set ('enable_team_account_billing')
)
);
/**
* -------------------------------------------------------
* Section: Order Items
* We create the schema for the order items. Order items are the items in an order.
* -------------------------------------------------------
*/
create table if not exists
public.order_items (
id text not null primary key,
order_id text references public.orders (id) on delete cascade not null,
product_id text not null,
variant_id text not null,
price_amount numeric,
quantity integer not null default 1,
created_at timestamptz not null default current_timestamp,
updated_at timestamptz not null default current_timestamp,
unique (order_id, product_id, variant_id)
);
comment on table public.order_items is 'The items in an order';
comment on column public.order_items.order_id is 'The order the item is for';
comment on column public.order_items.order_id is 'The order the item is for';
comment on column public.order_items.product_id is 'The product ID for the item';
comment on column public.order_items.variant_id is 'The variant ID for the item';
comment on column public.order_items.price_amount is 'The price amount for the item';
comment on column public.order_items.quantity is 'The quantity of the item';
comment on column public.order_items.created_at is 'The creation date of the item';
comment on column public.order_items.updated_at is 'The last update date of the item';
-- Revoke all access to order_items table for authenticated users and service_role
revoke all on public.order_items
from
authenticated,
service_role;
-- Open up relevant access to order_items table for authenticated users and service_role
grant
select
on table public.order_items to authenticated,
service_role;
grant insert, update, delete on table public.order_items to service_role;
-- Indexes on the order_items table
create index ix_order_items_order_id on public.order_items (order_id);
-- RLS
alter table public.order_items enable row level security;
-- SELECT(order_items):
-- Users can read order items on an order they are a member of
create policy order_items_read_self on public.order_items for
select
to authenticated using (
exists (
select
1
from
public.orders
where
id = order_id
and (
account_id = (
select
auth.uid ()
)
or has_role_on_account (account_id)
)
)
);
-- Function "public.upsert_order"
-- Insert or update an order and its items when receiving a webhook from the billing provider
create
or replace function public.upsert_order (
target_account_id uuid,
target_customer_id varchar(255),
target_order_id text,
status public.payment_status,
billing_provider public.billing_provider,
total_amount numeric,
currency varchar(3),
line_items jsonb
) returns public.orders
set
search_path = '' as $$
declare
new_order public.orders;
new_billing_customer_id int;
begin
insert into public.billing_customers(
account_id,
provider,
customer_id)
values (
target_account_id,
billing_provider,
target_customer_id)
on conflict (
account_id,
provider,
customer_id)
do update set
provider = excluded.provider
returning
id into new_billing_customer_id;
insert into public.orders(
account_id,
billing_customer_id,
id,
status,
billing_provider,
total_amount,
currency)
values (
target_account_id,
new_billing_customer_id,
target_order_id,
status,
billing_provider,
total_amount,
currency)
on conflict (
id)
do update set
status = excluded.status,
total_amount = excluded.total_amount,
currency = excluded.currency
returning
* into new_order;
-- Upsert order items and delete ones that are not in the line_items array
with item_data as (
select
(line_item ->> 'id')::varchar as line_item_id,
(line_item ->> 'product_id')::varchar as prod_id,
(line_item ->> 'variant_id')::varchar as var_id,
(line_item ->> 'price_amount')::numeric as price_amt,
(line_item ->> 'quantity')::integer as qty
from
jsonb_array_elements(line_items) as line_item
),
line_item_ids as (
select line_item_id from item_data
),
deleted_items as (
delete from
public.order_items
where
public.order_items.order_id = new_order.id
and public.order_items.id not in (select line_item_id from line_item_ids)
returning *
)
insert into public.order_items(
id,
order_id,
product_id,
variant_id,
price_amount,
quantity)
select
line_item_id,
target_order_id,
prod_id,
var_id,
price_amt,
qty
from
item_data
on conflict (id)
do update set
price_amount = excluded.price_amount,
product_id = excluded.product_id,
variant_id = excluded.variant_id,
quantity = excluded.quantity;
return new_order;
end;
$$ language plpgsql;
grant
execute on function public.upsert_order (
uuid,
varchar,
text,
public.payment_status,
public.billing_provider,
numeric,
varchar,
jsonb
) to service_role;

View File

@@ -1,114 +0,0 @@
/**
* -------------------------------------------------------
* Section: Notifications
* We create the schema for the notifications. Notifications are the notifications for an account.
* -------------------------------------------------------
*/
create type public.notification_channel as enum('in_app', 'email');
create type public.notification_type as enum('info', 'warning', 'error');
create table if not exists
public.notifications (
id bigint generated always as identity primary key,
account_id uuid not null references public.accounts (id) on delete cascade,
type public.notification_type not null default 'info',
body varchar(5000) not null,
link varchar(255),
channel public.notification_channel not null default 'in_app',
dismissed boolean not null default false,
expires_at timestamptz default (now() + interval '1 month'),
created_at timestamptz not null default now()
);
comment on table notifications is 'The notifications for an account';
comment on column notifications.account_id is 'The account the notification is for (null for system messages)';
comment on column notifications.type is 'The type of the notification';
comment on column notifications.body is 'The body of the notification';
comment on column notifications.link is 'The link for the notification';
comment on column notifications.channel is 'The channel for the notification';
comment on column notifications.dismissed is 'Whether the notification has been dismissed';
comment on column notifications.expires_at is 'The expiry date for the notification';
comment on column notifications.created_at is 'The creation date for the notification';
-- Revoke all access to notifications table for authenticated users and service_role
revoke all on public.notifications
from
authenticated,
service_role;
-- Open up relevant access to notifications table for authenticated users and service_role
grant
select
,
update on table public.notifications to authenticated,
service_role;
grant insert on table public.notifications to service_role;
-- enable realtime
alter publication supabase_realtime
add table public.notifications;
-- Indexes
-- Indexes on the notifications table
-- index for selecting notifications for an account that are not dismissed and not expired
create index idx_notifications_account_dismissed on notifications (account_id, dismissed, expires_at);
-- RLS
alter table public.notifications enable row level security;
-- SELECT(notifications):
-- Users can read notifications on an account they are a member of
create policy notifications_read_self on public.notifications for
select
to authenticated using (
account_id = (
select
auth.uid ()
)
or has_role_on_account (account_id)
);
-- UPDATE(notifications):
-- Users can set notifications to read on an account they are a member of
create policy notifications_update_self on public.notifications
for update
to authenticated using (
account_id = (
select
auth.uid ()
)
or has_role_on_account (account_id)
);
-- Function "kit.update_notification_dismissed_status"
-- Make sure the only updatable field is the dismissed status and nothing else
create
or replace function kit.update_notification_dismissed_status () returns trigger
set
search_path to '' as $$
begin
old.dismissed := new.dismissed;
if (new is distinct from old) then
raise exception 'UPDATE of columns other than "dismissed" is forbidden';
end if;
return old;
end;
$$ language plpgsql;
-- add trigger when updating a notification to update the dismissed status
create trigger update_notification_dismissed_status before
update on public.notifications for each row
execute procedure kit.update_notification_dismissed_status ();

View File

@@ -1,349 +0,0 @@
/*
* -------------------------------------------------------
* Section: Nonces
* We create the schema for the nonces. Nonces are used to create one-time tokens for authentication purposes.
* -------------------------------------------------------
*/
create extension if not exists pg_cron;
-- Create a table to store one-time tokens (nonces)
CREATE TABLE IF NOT EXISTS public.nonces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_token TEXT NOT NULL, -- token sent to client (hashed)
nonce TEXT NOT NULL, -- token stored in DB (hashed)
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NULL, -- Optional to support anonymous tokens
purpose TEXT NOT NULL, -- e.g., 'password-reset', 'email-verification', etc.
-- Status fields
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
used_at TIMESTAMPTZ,
revoked BOOLEAN NOT NULL DEFAULT FALSE, -- For administrative revocation
revoked_reason TEXT, -- Reason for revocation if applicable
-- Audit fields
verification_attempts INTEGER NOT NULL DEFAULT 0, -- Track attempted uses
last_verification_at TIMESTAMPTZ, -- Timestamp of last verification attempt
last_verification_ip INET, -- For tracking verification source
last_verification_user_agent TEXT, -- For tracking client information
-- Extensibility fields
metadata JSONB DEFAULT '{}'::JSONB, -- optional metadata
scopes TEXT[] DEFAULT '{}' -- OAuth-style authorized scopes
);
-- Create indexes for efficient lookups
CREATE INDEX IF NOT EXISTS idx_nonces_status ON public.nonces (client_token, user_id, purpose, expires_at)
WHERE used_at IS NULL AND revoked = FALSE;
-- Enable Row Level Security (RLS)
ALTER TABLE public.nonces ENABLE ROW LEVEL SECURITY;
-- RLS policies
-- Users can view their own nonces for verification
CREATE POLICY "Users can read their own nonces"
ON public.nonces
FOR SELECT
USING (
user_id = (select auth.uid())
);
-- Create a function to create a nonce
-- Create a function to create a nonce
create or replace function public.create_nonce (
p_user_id UUID default null,
p_purpose TEXT default null,
p_expires_in_seconds INTEGER default 3600, -- 1 hour by default
p_metadata JSONB default null,
p_scopes text[] default null,
p_revoke_previous BOOLEAN default true -- New parameter to control automatic revocation
) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER
set
search_path to '' as $$
DECLARE
v_client_token TEXT;
v_nonce TEXT;
v_expires_at TIMESTAMPTZ;
v_id UUID;
v_plaintext_token TEXT;
v_revoked_count INTEGER;
BEGIN
-- Revoke previous tokens for the same user and purpose if requested
-- This only applies if a user ID is provided (not for anonymous tokens)
IF p_revoke_previous = TRUE AND p_user_id IS NOT NULL THEN
WITH revoked AS (
UPDATE public.nonces
SET
revoked = TRUE,
revoked_reason = 'Superseded by new token with same purpose'
WHERE
user_id = p_user_id
AND purpose = p_purpose
AND used_at IS NULL
AND revoked = FALSE
AND expires_at > NOW()
RETURNING 1
)
SELECT COUNT(*) INTO v_revoked_count FROM revoked;
END IF;
-- Generate a 6-digit token
v_plaintext_token := (100000 + floor(random() * 900000))::text;
v_client_token := extensions.crypt(v_plaintext_token, extensions.gen_salt('bf'));
-- Still generate a secure nonce for internal use
v_nonce := encode(extensions.gen_random_bytes(24), 'base64');
v_nonce := extensions.crypt(v_nonce, extensions.gen_salt('bf'));
-- Calculate expiration time
v_expires_at := NOW() + (p_expires_in_seconds * interval '1 second');
-- Insert the new nonce
INSERT INTO public.nonces (
client_token,
nonce,
user_id,
expires_at,
metadata,
purpose,
scopes
)
VALUES (
v_client_token,
v_nonce,
p_user_id,
v_expires_at,
COALESCE(p_metadata, '{}'::JSONB),
p_purpose,
COALESCE(p_scopes, '{}'::TEXT[])
)
RETURNING id INTO v_id;
-- Return the token information
-- Note: returning the plaintext token, not the hash
RETURN jsonb_build_object(
'id', v_id,
'token', v_plaintext_token,
'expires_at', v_expires_at,
'revoked_previous_count', COALESCE(v_revoked_count, 0)
);
END;
$$;
grant execute on function public.create_nonce to service_role;
-- Create a function to verify a nonce
create or replace function public.verify_nonce (
p_token TEXT,
p_purpose TEXT,
p_user_id UUID default null,
p_required_scopes text[] default null,
p_max_verification_attempts INTEGER default 5,
p_ip INET default null,
p_user_agent TEXT default null
) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER
set
SEARCH_PATH to '' as $$
DECLARE
v_nonce RECORD;
v_matching_count INTEGER;
BEGIN
-- Count how many matching tokens exist before verification attempt
SELECT COUNT(*)
INTO v_matching_count
FROM public.nonces
WHERE purpose = p_purpose;
-- Update verification attempt counter and tracking info for all matching tokens
UPDATE public.nonces
SET verification_attempts = verification_attempts + 1,
last_verification_at = NOW(),
last_verification_ip = COALESCE(p_ip, last_verification_ip),
last_verification_user_agent = COALESCE(p_user_agent, last_verification_user_agent)
WHERE client_token = extensions.crypt(p_token, client_token)
AND purpose = p_purpose;
-- Find the nonce by token and purpose
-- Modified to handle user-specific tokens better
SELECT *
INTO v_nonce
FROM public.nonces
WHERE client_token = extensions.crypt(p_token, client_token)
AND purpose = p_purpose
-- Only apply user_id filter if the token was created for a specific user
AND (
-- Case 1: Anonymous token (user_id is NULL in DB)
(user_id IS NULL)
OR
-- Case 2: User-specific token (check if user_id matches)
(user_id = p_user_id)
)
AND used_at IS NULL
AND NOT revoked
AND expires_at > NOW();
-- Check if nonce exists
IF v_nonce.id IS NULL THEN
RETURN jsonb_build_object(
'valid', false,
'message', 'Invalid or expired token'
);
END IF;
-- Check if max verification attempts exceeded
IF p_max_verification_attempts > 0 AND v_nonce.verification_attempts > p_max_verification_attempts THEN
-- Automatically revoke the token
UPDATE public.nonces
SET revoked = TRUE,
revoked_reason = 'Maximum verification attempts exceeded'
WHERE id = v_nonce.id;
RETURN jsonb_build_object(
'valid', false,
'message', 'Token revoked due to too many verification attempts',
'max_attempts_exceeded', true
);
END IF;
-- Check scopes if required
IF p_required_scopes IS NOT NULL AND array_length(p_required_scopes, 1) > 0 THEN
-- Fix scope validation to properly check if token scopes contain all required scopes
-- Using array containment check: array1 @> array2 (array1 contains array2)
IF NOT (v_nonce.scopes @> p_required_scopes) THEN
RETURN jsonb_build_object(
'valid', false,
'message', 'Token does not have required permissions',
'token_scopes', v_nonce.scopes,
'required_scopes', p_required_scopes
);
END IF;
END IF;
-- Mark nonce as used
UPDATE public.nonces
SET used_at = NOW()
WHERE id = v_nonce.id;
-- Return success with metadata
RETURN jsonb_build_object(
'valid', true,
'user_id', v_nonce.user_id,
'metadata', v_nonce.metadata,
'scopes', v_nonce.scopes,
'purpose', v_nonce.purpose
);
END;
$$;
grant
execute on function public.verify_nonce to authenticated,
service_role;
-- Create a function to revoke a nonce
CREATE OR REPLACE FUNCTION public.revoke_nonce(
p_id UUID,
p_reason TEXT DEFAULT NULL
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $$
DECLARE
v_affected_rows INTEGER;
BEGIN
UPDATE public.nonces
SET
revoked = TRUE,
revoked_reason = p_reason
WHERE
id = p_id
AND used_at IS NULL
AND NOT revoked
RETURNING 1 INTO v_affected_rows;
RETURN v_affected_rows > 0;
END;
$$;
grant execute on function public.revoke_nonce to service_role;
-- Create a function to clean up expired nonces
CREATE OR REPLACE FUNCTION kit.cleanup_expired_nonces(
p_older_than_days INTEGER DEFAULT 1,
p_include_used BOOLEAN DEFAULT TRUE,
p_include_revoked BOOLEAN DEFAULT TRUE
)
RETURNS INTEGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $$
DECLARE
v_count INTEGER;
BEGIN
-- Count and delete expired or used nonces based on parameters
WITH deleted AS (
DELETE FROM public.nonces
WHERE
(
-- Expired and unused tokens
(expires_at < NOW() AND used_at IS NULL)
-- Used tokens older than specified days (if enabled)
OR (p_include_used = TRUE AND used_at < NOW() - (p_older_than_days * interval '1 day'))
-- Revoked tokens older than specified days (if enabled)
OR (p_include_revoked = TRUE AND revoked = TRUE AND created_at < NOW() - (p_older_than_days * interval '1 day'))
)
RETURNING 1
)
SELECT COUNT(*) INTO v_count FROM deleted;
RETURN v_count;
END;
$$;
-- Create a function to get token status (for administrative use)
CREATE OR REPLACE FUNCTION public.get_nonce_status(
p_id UUID
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $$
DECLARE
v_nonce public.nonces;
BEGIN
SELECT * INTO v_nonce FROM public.nonces WHERE id = p_id;
IF v_nonce.id IS NULL THEN
RETURN jsonb_build_object('exists', false);
END IF;
RETURN jsonb_build_object(
'exists', true,
'purpose', v_nonce.purpose,
'user_id', v_nonce.user_id,
'created_at', v_nonce.created_at,
'expires_at', v_nonce.expires_at,
'used_at', v_nonce.used_at,
'revoked', v_nonce.revoked,
'revoked_reason', v_nonce.revoked_reason,
'verification_attempts', v_nonce.verification_attempts,
'last_verification_at', v_nonce.last_verification_at,
'last_verification_ip', v_nonce.last_verification_ip,
'is_valid', (v_nonce.used_at IS NULL AND NOT v_nonce.revoked AND v_nonce.expires_at > NOW())
);
END;
$$;
-- Comments for documentation
COMMENT ON TABLE public.nonces IS 'Table for storing one-time tokens with enhanced security and audit features';
COMMENT ON FUNCTION public.create_nonce IS 'Creates a new one-time token for a specific purpose with enhanced options';
COMMENT ON FUNCTION public.verify_nonce IS 'Verifies a one-time token, checks scopes, and marks it as used';
COMMENT ON FUNCTION public.revoke_nonce IS 'Administratively revokes a token to prevent its use';
COMMENT ON FUNCTION kit.cleanup_expired_nonces IS 'Cleans up expired, used, or revoked tokens based on parameters';
COMMENT ON FUNCTION public.get_nonce_status IS 'Retrieves the status of a token for administrative purposes';

View File

@@ -1,145 +0,0 @@
/*
* -------------------------------------------------------
* Section: MFA
* We create the policies and functions to enforce MFA
* -------------------------------------------------------
*/
/*
* public.is_aal2
* Check if the user has aal2 access
*/
create
or replace function public.is_aal2() returns boolean
set
search_path = '' as
$$
declare
is_aal2 boolean;
begin
select auth.jwt() ->> 'aal' = 'aal2' into is_aal2;
return coalesce(is_aal2, false);
end
$$ language plpgsql;
-- Grant access to the function to authenticated users
grant execute on function public.is_aal2() to authenticated;
/*
* public.is_super_admin
* Check if the user is a super admin.
* A Super Admin is a user that has the role 'super-admin' and has MFA enabled.
*/
create
or replace function public.is_super_admin() returns boolean
set
search_path = '' as
$$
declare
is_super_admin boolean;
begin
if not public.is_aal2() then
return false;
end if;
select (auth.jwt() ->> 'app_metadata')::jsonb ->> 'role' = 'super-admin' into is_super_admin;
return coalesce(is_super_admin, false);
end
$$ language plpgsql;
-- Grant access to the function to authenticated users
grant execute on function public.is_super_admin() to authenticated;
/*
* public.is_mfa_compliant
* Check if the user meets MFA requirements if they have MFA enabled.
* If the user has MFA enabled, then the user must have aal2 enabled. Otherwise, the user must have aal1 enabled (default behavior).
*/
create or replace function public.is_mfa_compliant() returns boolean
set search_path = '' as
$$
begin
return array[(select auth.jwt()->>'aal')] <@ (
select
case
when count(id) > 0 then array['aal2']
else array['aal1', 'aal2']
end as aal
from auth.mfa_factors
where ((select auth.uid()) = auth.mfa_factors.user_id) and auth.mfa_factors.status = 'verified'
);
end
$$ language plpgsql security definer;
-- Grant access to the function to authenticated users
grant execute on function public.is_mfa_compliant() to authenticated;
-- MFA Restrictions:
-- the following policies are applied to the tables as a
-- restrictive policy to ensure that if MFA is enabled, then the policy will be applied.
-- For users that have not enabled MFA, the policy will not be applied and will keep the default behavior.
-- Restrict access to accounts if MFA is enabled
create policy restrict_mfa_accounts
on public.accounts
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to accounts memberships if MFA is enabled
create policy restrict_mfa_accounts_memberships
on public.accounts_memberships
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to subscriptions if MFA is enabled
create policy restrict_mfa_subscriptions
on public.subscriptions
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to subscription items if MFA is enabled
create policy restrict_mfa_subscription_items
on public.subscription_items
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to role permissions if MFA is enabled
create policy restrict_mfa_role_permissions
on public.role_permissions
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to invitations if MFA is enabled
create policy restrict_mfa_invitations
on public.invitations
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to orders if MFA is enabled
create policy restrict_mfa_orders
on public.orders
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to orders items if MFA is enabled
create policy restrict_mfa_order_items
on public.order_items
as restrictive
to authenticated
using (public.is_mfa_compliant());
-- Restrict access to orders if MFA is enabled
create policy restrict_mfa_notifications
on public.notifications
as restrictive
to authenticated
using (public.is_mfa_compliant());

View File

@@ -1,73 +0,0 @@
/*
* -------------------------------------------------------
* Section: Super Admin
* We create the policies and functions to enforce super admin access
* -------------------------------------------------------
*/
-- the following policies are applied to the tables as a permissive policy to ensure that
-- super admins can access all tables (view only).
-- Allow Super Admins to access the accounts table
create policy super_admins_access_accounts
on public.accounts
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the accounts memberships table
create policy super_admins_access_accounts_memberships
on public.accounts_memberships
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the subscriptions table
create policy super_admins_access_subscriptions
on public.subscriptions
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the subscription items table
create policy super_admins_access_subscription_items
on public.subscription_items
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the invitations items table
create policy super_admins_access_invitations
on public.invitations
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the orders table
create policy super_admins_access_orders
on public.orders
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the order items table
create policy super_admins_access_order_items
on public.order_items
as permissive
for select
to authenticated
using (public.is_super_admin());
-- Allow Super Admins to access the role permissions table
create policy super_admins_access_role_permissions
on public.role_permissions
as permissive
for select
to authenticated
using (public.is_super_admin());

View File

@@ -1,126 +0,0 @@
/*
* -------------------------------------------------------
* Section: Account Functions
* We create the schema for the functions. Functions are the custom functions for the application.
* -------------------------------------------------------
*/
--
-- VIEW "user_account_workspace":
-- we create a view to load the general app data for the authenticated
-- user which includes the user accounts and memberships
create or replace view
public.user_account_workspace
with
(security_invoker = true) as
select
accounts.id as id,
accounts.name as name,
accounts.picture_url as picture_url,
(
select
status
from
public.subscriptions
where
account_id = accounts.id
limit
1
) as subscription_status
from
public.accounts
where
primary_owner_user_id = (select auth.uid ())
and accounts.is_personal_account = true
limit
1;
grant
select
on public.user_account_workspace to authenticated,
service_role;
--
-- VIEW "user_accounts":
-- we create a view to load the user's accounts and memberships
-- useful to display the user's accounts in the app
create or replace view
public.user_accounts (id, name, picture_url, slug, role)
with
(security_invoker = true) as
select
account.id,
account.name,
account.picture_url,
account.slug,
membership.account_role
from
public.accounts account
join public.accounts_memberships membership on account.id = membership.account_id
where
membership.user_id = (select auth.uid ())
and account.is_personal_account = false
and account.id in (
select
account_id
from
public.accounts_memberships
where
user_id = (select auth.uid ())
);
grant
select
on public.user_accounts to authenticated,
service_role;
--
-- Function "public.team_account_workspace"
-- Load all the data for a team account workspace
create or replace function public.team_account_workspace(account_slug text)
returns table (
id uuid,
name varchar(255),
picture_url varchar(1000),
slug text,
role varchar(50),
role_hierarchy_level int,
primary_owner_user_id uuid,
subscription_status public.subscription_status,
permissions public.app_permissions[]
)
set search_path to ''
as $$
begin
return QUERY
select
accounts.id,
accounts.name,
accounts.picture_url,
accounts.slug,
accounts_memberships.account_role,
roles.hierarchy_level,
accounts.primary_owner_user_id,
subscriptions.status,
array_agg(role_permissions.permission)
from
public.accounts
join public.accounts_memberships on accounts.id = accounts_memberships.account_id
left join public.subscriptions on accounts.id = subscriptions.account_id
join public.roles on accounts_memberships.account_role = roles.name
left join public.role_permissions on accounts_memberships.account_role = role_permissions.role
where
accounts.slug = account_slug
and public.accounts_memberships.user_id = (select auth.uid())
group by
accounts.id,
accounts_memberships.account_role,
subscriptions.status,
roles.hierarchy_level;
end;
$$ language plpgsql;
grant
execute on function public.team_account_workspace (text) to authenticated,
service_role;

View File

@@ -1,50 +0,0 @@
/*
* -------------------------------------------------------
* Section: Storage
* We create the schema for the storage
* -------------------------------------------------------
*/
-- Account Image
insert into
storage.buckets (id, name, PUBLIC)
values
('account_image', 'account_image', true);
-- Function: get the storage filename as a UUID.
-- Useful if you want to name files with UUIDs related to an account
create
or replace function kit.get_storage_filename_as_uuid (name text) returns uuid
set
search_path = '' as $$
begin
return replace(storage.filename(name), concat('.',
storage.extension(name)), '')::uuid;
end;
$$ language plpgsql;
grant
execute on function kit.get_storage_filename_as_uuid (text) to authenticated,
service_role;
-- RLS policies for storage bucket account_image
create policy account_image on storage.objects for all using (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
)
with check (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_permission(
auth.uid(),
kit.get_storage_filename_as_uuid(name),
'settings.manage'
)
)
);

View File

@@ -1,47 +0,0 @@
/*
* -------------------------------------------------------
* Section: Roles Seed
* We create the roles and role permissions seed data
* -------------------------------------------------------
*/
-- Seed the roles table with default roles 'owner' and 'member'
insert into public.roles(
name,
hierarchy_level)
values (
'owner',
1);
insert into public.roles(
name,
hierarchy_level)
values (
'member',
2);
-- We seed the role_permissions table with the default roles and permissions
insert into public.role_permissions(
role,
permission)
values (
'owner',
'roles.manage'),
(
'owner',
'billing.manage'),
(
'owner',
'settings.manage'),
(
'owner',
'members.manage'),
(
'owner',
'invites.manage'),
(
'member',
'settings.manage'),
(
'member',
'invites.manage');

View File

@@ -1,315 +0,0 @@
-- WEBHOOKS SEED
-- PLEASE NOTE: These webhooks are only for development purposes. Leave them as they are or add new ones.
-- These webhooks are only for development purposes.
-- In production, you should manually create webhooks in the Supabase dashboard (or create a migration to do so).
-- We don't do it because you'll need to manually add your webhook URL and secret key.
-- this webhook will be triggered after deleting an account
create trigger "accounts_teardown"
after delete
on "public"."accounts"
for each row
execute function "supabase_functions"."http_request"(
'http://host.docker.internal:3000/api/db/webhook',
'POST',
'{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}',
'{}',
'5000'
);
-- this webhook will be triggered after a delete on the subscriptions table
-- which should happen when a user deletes their account (and all their subscriptions)
create trigger "subscriptions_delete"
after delete
on "public"."subscriptions"
for each row
execute function "supabase_functions"."http_request"(
'http://host.docker.internal:3000/api/db/webhook',
'POST',
'{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}',
'{}',
'5000'
);
-- this webhook will be triggered after every insert on the invitations table
-- which should happen when a user invites someone to their account
create trigger "invitations_insert"
after insert
on "public"."invitations"
for each row
execute function "supabase_functions"."http_request"(
'http://host.docker.internal:3000/api/db/webhook',
'POST',
'{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}',
'{}',
'5000'
);
-- DATA SEED
-- This is a data dump for testing purposes. It should be used to seed the database with data for testing.
--
-- Data for Name: flow_state; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: users; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
INSERT INTO "auth"."users" ("instance_id", "id", "aud", "role", "email", "encrypted_password", "email_confirmed_at",
"invited_at", "confirmation_token", "confirmation_sent_at", "recovery_token",
"recovery_sent_at", "email_change_token_new", "email_change", "email_change_sent_at",
"last_sign_in_at", "raw_app_meta_data", "raw_user_meta_data", "is_super_admin",
"created_at", "updated_at", "phone", "phone_confirmed_at", "phone_change",
"phone_change_token", "phone_change_sent_at", "email_change_token_current",
"email_change_confirm_status", "banned_until", "reauthentication_token",
"reauthentication_sent_at", "is_sso_user", "deleted_at", "is_anonymous")
VALUES ('00000000-0000-0000-0000-000000000000', 'b73eb03e-fb7a-424d-84ff-18e2791ce0b4', 'authenticated',
'authenticated', 'custom@makerkit.dev', '$2a$10$b3ZPpU6TU3or30QzrXnZDuATPAx2pPq3JW.sNaneVY3aafMSuR4yi',
'2024-04-20 08:38:00.860548+00', NULL, '', '2024-04-20 08:37:43.343769+00', '', NULL, '', '', NULL,
'2024-04-20 08:38:00.93864+00', '{"provider": "email", "providers": ["email"]}',
'{"sub": "b73eb03e-fb7a-424d-84ff-18e2791ce0b4", "email": "custom@makerkit.dev", "email_verified": false, "phone_verified": false}',
NULL, '2024-04-20 08:37:43.3385+00', '2024-04-20 08:38:00.942809+00', NULL, NULL, '', '', NULL, '', 0, NULL, '',
NULL, false, NULL, false),
('00000000-0000-0000-0000-000000000000', '31a03e74-1639-45b6-bfa7-77447f1a4762', 'authenticated',
'authenticated', 'test@makerkit.dev', '$2a$10$NaMVRrI7NyfwP.AfAVWt6O/abulGnf9BBqwa6DqdMwXMvOCGpAnVO',
'2024-04-20 08:20:38.165331+00', NULL, '', NULL, '', NULL, '', '', NULL, '2024-04-20 09:36:02.521776+00',
'{"provider": "email", "providers": ["email"], "role": "super-admin"}',
'{"sub": "31a03e74-1639-45b6-bfa7-77447f1a4762", "email": "test@makerkit.dev", "email_verified": false, "phone_verified": false}',
NULL, '2024-04-20 08:20:34.459113+00', '2024-04-20 10:07:48.554125+00', NULL, NULL, '', '', NULL, '', 0, NULL,
'', NULL, false, NULL, false),
('00000000-0000-0000-0000-000000000000', '5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', 'authenticated',
'authenticated', 'owner@makerkit.dev', '$2a$10$D6arGxWJShy8q4RTW18z7eW0vEm2hOxEUovUCj5f3NblyHfamm5/a',
'2024-04-20 08:36:37.517993+00', NULL, '', '2024-04-20 08:36:27.639648+00', '', NULL, '', '', NULL,
'2024-04-20 08:36:37.614337+00', '{"provider": "email", "providers": ["email"]}',
'{"sub": "5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf", "email": "owner@makerkit.dev", "email_verified": false, "phone_verified": false}',
NULL, '2024-04-20 08:36:27.630379+00', '2024-04-20 08:36:37.617955+00', NULL, NULL, '', '', NULL, '', 0, NULL,
'', NULL, false, NULL, false),
('00000000-0000-0000-0000-000000000000', '6b83d656-e4ab-48e3-a062-c0c54a427368', 'authenticated',
'authenticated', 'member@makerkit.dev', '$2a$10$6h/x.AX.6zzphTfDXIJMzuYx13hIYEi/Iods9FXH19J2VxhsLycfa',
'2024-04-20 08:41:15.376778+00', NULL, '', '2024-04-20 08:41:08.689674+00', '', NULL, '', '', NULL,
'2024-04-20 08:41:15.484606+00', '{"provider": "email", "providers": ["email"]}',
'{"sub": "6b83d656-e4ab-48e3-a062-c0c54a427368", "email": "member@makerkit.dev", "email_verified": false, "phone_verified": false}',
NULL, '2024-04-20 08:41:08.683395+00', '2024-04-20 08:41:15.485494+00', NULL, NULL, '', '', NULL, '', 0, NULL,
'', NULL, false, NULL, false),
('00000000-0000-0000-0000-000000000000', 'c5b930c9-0a76-412e-a836-4bc4849a3270', 'authenticated',
'authenticated', 'super-admin@makerkit.dev',
'$2a$10$gzxQw3vaVni8Ke9UVcn6ueWh674.6xImf6/yWYNc23BSeYdE9wmki', '2025-02-24 13:25:11.176987+00', null, '',
'2025-02-24 13:25:01.649714+00', '', null, '', '', null, '2025-02-24 13:25:11.17957+00',
'{"provider": "email", "providers": ["email"], "role": "super-admin"}',
'{"sub": "c5b930c9-0a76-412e-a836-4bc4849a3270", "email": "super-admin@makerkit.dev", "email_verified": true, "phone_verified": false}',
null, '2025-02-24 13:25:01.646641+00', '2025-02-24 13:25:11.181332+00', null, null, '', '', null
, '', '0', null, '', null, 'false', null, 'false');
--
-- Data for Name: identities; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
INSERT INTO "auth"."identities" ("provider_id", "user_id", "identity_data", "provider", "last_sign_in_at", "created_at",
"updated_at", "id")
VALUES ('31a03e74-1639-45b6-bfa7-77447f1a4762', '31a03e74-1639-45b6-bfa7-77447f1a4762',
'{"sub": "31a03e74-1639-45b6-bfa7-77447f1a4762", "email": "test@makerkit.dev", "email_verified": false, "phone_verified": false}',
'email', '2024-04-20 08:20:34.46275+00', '2024-04-20 08:20:34.462773+00', '2024-04-20 08:20:34.462773+00',
'9bb58bad-24a4-41a8-9742-1b5b4e2d8abd'),
('5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', '5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf',
'{"sub": "5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf", "email": "owner@makerkit.dev", "email_verified": false, "phone_verified": false}',
'email', '2024-04-20 08:36:27.637388+00', '2024-04-20 08:36:27.637409+00', '2024-04-20 08:36:27.637409+00',
'090598a1-ebba-4879-bbe3-38d517d5066f'),
('b73eb03e-fb7a-424d-84ff-18e2791ce0b4', 'b73eb03e-fb7a-424d-84ff-18e2791ce0b4',
'{"sub": "b73eb03e-fb7a-424d-84ff-18e2791ce0b4", "email": "custom@makerkit.dev", "email_verified": false, "phone_verified": false}',
'email', '2024-04-20 08:37:43.342194+00', '2024-04-20 08:37:43.342218+00', '2024-04-20 08:37:43.342218+00',
'4392e228-a6d8-4295-a7d6-baed50c33e7c'),
('6b83d656-e4ab-48e3-a062-c0c54a427368', '6b83d656-e4ab-48e3-a062-c0c54a427368',
'{"sub": "6b83d656-e4ab-48e3-a062-c0c54a427368", "email": "member@makerkit.dev", "email_verified": false, "phone_verified": false}',
'email', '2024-04-20 08:41:08.687948+00', '2024-04-20 08:41:08.687982+00', '2024-04-20 08:41:08.687982+00',
'd122aca5-4f29-43f0-b1b1-940b000638db'),
('c5b930c9-0a76-412e-a836-4bc4849a3270', 'c5b930c9-0a76-412e-a836-4bc4849a3270',
'{"sub": "c5b930c9-0a76-412e-a836-4bc4849a3270", "email": "super-admin@makerkit.dev", "email_verified": true, "phone_verified": false}',
'email', '2025-02-24 13:25:01.646641+00', '2025-02-24 13:25:11.181332+00', '2025-02-24 13:25:11.181332+00',
'c5b930c9-0a76-412e-a836-4bc4849a3270');
--
-- Data for Name: instances; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: sessions; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: mfa_amr_claims; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: mfa_factors; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: mfa_challenges; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: refresh_tokens; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: sso_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: saml_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: saml_relay_states; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: sso_domains; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: key; Type: TABLE DATA; Schema: pgsodium; Owner: supabase_admin
--
--
-- Data for Name: accounts; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."accounts" ("id", "primary_owner_user_id", "name", "slug", "email", "is_personal_account",
"updated_at", "created_at", "created_by", "updated_by", "picture_url", "public_data")
VALUES ('5deaa894-2094-4da3-b4fd-1fada0809d1c', '31a03e74-1639-45b6-bfa7-77447f1a4762', 'Makerkit', 'makerkit', NULL,
false, NULL, NULL, NULL, NULL, NULL, '{}');
--
-- Data for Name: roles; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."roles" ("name", "hierarchy_level")
VALUES ('custom-role', 4);
--
-- Data for Name: accounts_memberships; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."accounts_memberships" ("user_id", "account_id", "account_role", "created_at", "updated_at",
"created_by", "updated_by")
VALUES ('31a03e74-1639-45b6-bfa7-77447f1a4762', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'owner',
'2024-04-20 08:21:16.802867+00', '2024-04-20 08:21:16.802867+00', NULL, NULL),
('5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'owner',
'2024-04-20 08:36:44.21028+00', '2024-04-20 08:36:44.21028+00', NULL, NULL),
('b73eb03e-fb7a-424d-84ff-18e2791ce0b4', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'custom-role',
'2024-04-20 08:38:02.50993+00', '2024-04-20 08:38:02.50993+00', NULL, NULL),
('6b83d656-e4ab-48e3-a062-c0c54a427368', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'member',
'2024-04-20 08:41:17.833709+00', '2024-04-20 08:41:17.833709+00', NULL, NULL);
-- MFA Factors
INSERT INTO "auth"."mfa_factors" ("id", "user_id", "friendly_name", "factor_type", "status", "created_at", "updated_at",
"secret", "phone", "last_challenged_at")
VALUES ('659e3b57-1128-4d26-8757-f714fd073fc4', 'c5b930c9-0a76-412e-a836-4bc4849a3270', 'iPhone', 'totp', 'verified',
'2025-02-24 13:23:55.5805+00', '2025-02-24 13:24:32.591999+00', 'NHOHJVGPO3R3LKVPRMNIYLCDMBHUM2SE', null,
'2025-02-24 13:24:32.563314+00');
--
-- Data for Name: billing_customers; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: invitations; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: orders; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: order_items; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: subscriptions; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: subscription_items; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: buckets; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
--
--
-- Data for Name: objects; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
--
--
-- Data for Name: s3_multipart_uploads; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
--
--
-- Data for Name: s3_multipart_uploads_parts; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
--
--
-- Data for Name: hooks; Type: TABLE DATA; Schema: supabase_functions; Owner: supabase_functions_admin
--
--
-- Data for Name: secrets; Type: TABLE DATA; Schema: vault; Owner: supabase_admin
--
--
-- Name: refresh_tokens_id_seq; Type: SEQUENCE SET; Schema: auth; Owner: supabase_auth_admin
--
SELECT pg_catalog.setval('"auth"."refresh_tokens_id_seq"', 5, true);
--
-- Name: billing_customers_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('"public"."billing_customers_id_seq"', 1, false);
--
-- Name: invitations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('"public"."invitations_id_seq"', 19, true);
--
-- Name: role_permissions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('"public"."role_permissions_id_seq"', 7, true);
--
-- Name: hooks_id_seq; Type: SEQUENCE SET; Schema: supabase_functions; Owner: supabase_functions_admin
--
SELECT pg_catalog.setval('"supabase_functions"."hooks_id_seq"', 19, true);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,74 +0,0 @@
create extension if not exists http with schema extensions;
create extension if not exists pg_tle;
select
no_plan ();
create or replace function install_extensions()
returns void
as $$
declare
installed boolean;
begin
select exists (
select
1
from
pg_catalog.pg_extension
where
extname = 'supabase-dbdev'
) into installed;
if installed then
return;
end if;
perform
pgtle.install_extension(
'supabase-dbdev',
resp.contents ->> 'version',
'PostgreSQL package manager',
resp.contents ->> 'sql'
)
from http(
(
'GET',
'https://api.database.dev/rest/v1/'
|| 'package_versions?select=sql,version'
|| '&package_name=eq.supabase-dbdev'
|| '&order=version.desc'
|| '&limit=1',
array[
('apiKey', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhtdXB0cHBsZnZpaWZyYndtbXR2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODAxMDczNzIsImV4cCI6MTk5NTY4MzM3Mn0.z2CN0mvO2No8wSi46Gw59DFGCTJrzM0AQKsu_5k134s')::http_header
],
null,
null
)
) x,
lateral (
select
((row_to_json(x) -> 'content') #>> '{}')::json -> 0
) resp(contents);
create extension if not exists "supabase-dbdev";
perform dbdev.install('supabase-dbdev');
perform dbdev.install('basejump-supabase_test_helpers');
end
$$ language plpgsql;
select install_extensions();
select has_column(
'auth',
'users',
'id',
'id should exist'
);
select
*
from
finish ();
rollback;

View File

@@ -1,151 +0,0 @@
create schema if not exists makerkit;
-- anon, authenticated, and service_role should have access to makerkit schema
grant USAGE on schema makerkit to anon, authenticated, service_role;
-- Don't allow public to execute any functions in the makerkit schema
alter default PRIVILEGES in schema makerkit revoke execute on FUNCTIONS from public;
-- Grant execute to anon, authenticated, and service_role for testing purposes
alter default PRIVILEGES in schema makerkit grant execute on FUNCTIONS to anon,
authenticated, service_role;
create or replace function makerkit.get_id_by_identifier(
identifier text
)
returns uuid
as $$
begin
return (select id from auth.users where raw_user_meta_data->>'test_identifier' = identifier);
end;
$$ language PLPGSQL;
create or replace function makerkit.set_identifier(
identifier text,
user_email text
)
returns text
security definer
set search_path = auth, pg_temp
as
$$
begin
update auth.users
set raw_user_meta_data = jsonb_build_object('test_identifier', identifier)
where email = user_email;
return identifier;
end;
$$ language PLPGSQL;
create or replace function makerkit.get_account_by_slug(
account_slug text
)
returns setof accounts
as
$$
begin
return query
select *
from accounts
where slug = account_slug;
end;
$$ language PLPGSQL;
create or replace function makerkit.authenticate_as(
identifier text
) returns void
as
$$
begin
perform tests.authenticate_as(identifier);
perform makerkit.set_session_aal('aal1');
end;
$$ language plpgsql;
create or replace function makerkit.get_account_id_by_slug(
account_slug text
)
returns uuid
as
$$
begin
return
(select id
from accounts
where slug = account_slug);
end;
$$ language PLPGSQL;
create or replace function makerkit.set_mfa_factor(
identifier text = gen_random_uuid()
)
returns void
as
$$
begin
insert into "auth"."mfa_factors" ("id", "user_id", "friendly_name", "factor_type", "status", "created_at", "updated_at", "secret")
values (gen_random_uuid(), auth.uid(), identifier, 'totp', 'verified', '2025-02-24 09:48:18.402031+00', '2025-02-24 09:48:18.402031+00',
'HOWQFBA7KBDDRSBNMGFYZAFNPRSZ62I5');
end;
$$ language plpgsql security definer;
create or replace function makerkit.set_session_aal(session_aal auth.aal_level)
returns void
as
$$
begin
perform set_config('request.jwt.claims', json_build_object(
'sub', current_setting('request.jwt.claims')::json ->> 'sub',
'email', current_setting('request.jwt.claims')::json ->> 'email',
'phone', current_setting('request.jwt.claims')::json ->> 'phone',
'user_metadata', current_setting('request.jwt.claims')::json ->> 'user_metadata',
'app_metadata', current_setting('request.jwt.claims')::json ->> 'app_metadata',
'aal', session_aal)::text, true);
end;
$$ language plpgsql;
create or replace function makerkit.set_super_admin() returns void
as
$$
begin
perform set_config('request.jwt.claims', json_build_object(
'sub', current_setting('request.jwt.claims')::json ->> 'sub',
'email', current_setting('request.jwt.claims')::json ->> 'email',
'phone', current_setting('request.jwt.claims')::json ->> 'phone',
'user_metadata', current_setting('request.jwt.claims')::json ->> 'user_metadata',
'app_metadata', json_build_object('role', 'super-admin'),
'aal', current_setting('request.jwt.claims')::json ->> 'aal'
)::text, true);
end;
$$ language plpgsql;
begin;
select plan(1);
select is_empty($$
select
*
from
makerkit.get_account_by_slug('test') $$,
'get_account_by_slug should return an empty set when the account does not exist'
);
select *
from
finish();
rollback;

View File

@@ -1,104 +0,0 @@
BEGIN;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
--- we insert a user into auth.users and return the id into user_id to use
select tests.create_supabase_user('test1', 'test1@test.com');
select tests.create_supabase_user('test2');
-- Create an team account
select makerkit.authenticate_as('test1');
select public.create_team_account('Test');
-- the owner account has permissions to manage members
select row_eq(
$$ select public.has_permission(
auth.uid(), makerkit.get_account_id_by_slug('test'), 'members.manage'::app_permissions) $$,
row(true::boolean),
'The owner of the team account should have the members.manage permission'
);
-- the owner account has permissions to manage billing
select row_eq(
$$ select public.has_permission(
auth.uid(), makerkit.get_account_id_by_slug('test'), 'billing.manage'::app_permissions) $$,
row(true::boolean),
'The owner of the team account should have the billing.manage permission'
);
-- Foreigner should not have permissions to manage members
select makerkit.authenticate_as('test2');
select row_eq(
$$ select public.has_permission(
auth.uid(), makerkit.get_account_id_by_slug('test'), 'members.manage'::app_permissions) $$,
row(false::boolean),
'Foreigners should not have the members.manage permission'
);
-- Custom roles
-- New roles created for the app
set local role postgres;
-- the name should be unique
select throws_ok(
$$ insert into public.roles (name, hierarchy_level) values ('owner', 4) $$,
'duplicate key value violates unique constraint "roles_pkey"'
);
-- the hierarchy level should be unique
select throws_ok(
$$ insert into public.roles (name, hierarchy_level) values ('custom-role-2', 1) $$,
'duplicate key value violates unique constraint "roles_hierarchy_level_key"'
);
-- Custom Account Role
set local role postgres;
-- the names should be unique
select throws_ok(
$$ insert into public.roles (name, hierarchy_level) values ('owner', 1) $$,
'duplicate key value violates unique constraint "roles_pkey"'
);
-- update user role to custom role
update public.accounts_memberships
set account_role = 'custom-role'
where account_id = makerkit.get_account_id_by_slug('test')
and user_id = tests.get_supabase_uid('test1');
set local role postgres;
-- insert permissions for the custom role
insert into public.role_permissions (role, permission) values ('custom-role', 'members.manage');
select makerkit.authenticate_as('test1');
-- the custom role does not have permissions to manage billing
select row_eq(
$$ select public.has_permission(
auth.uid(), makerkit.get_account_id_by_slug('test'), 'billing.manage'::app_permissions) $$,
row(false::boolean),
'The custom role should not have the billing.manage permission'
);
-- the custom role can manage members
select row_eq(
$$ select public.has_permission(
auth.uid(), makerkit.get_account_id_by_slug('test'), 'members.manage'::app_permissions) $$,
row(true::boolean),
'The custom role should have the members.manage permission'
);
select * from finish();
rollback;

View File

@@ -1,128 +0,0 @@
BEGIN;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
--- we insert a user into auth.users and return the id into user_id to use
select tests.create_supabase_user('test1', 'test1@test.com');
select tests.create_supabase_user('test2');
-- Create an team account
select makerkit.authenticate_as('test1');
select public.create_team_account('Test');
select public.create_team_account('Test');
select public.create_team_account('Test');
-- should automatically create slugs for the accounts
select row_eq(
$$ select slug from public.accounts where name = 'Test' and slug = 'test' $$,
row('test'::text),
'The first team account should automatically create a slug named "test"'
);
select row_eq(
$$ select slug from public.accounts where name = 'Test' and slug = 'test-1' $$,
row('test-1'::text),
'The second team account should automatically create a slug named "test-1"'
);
select row_eq(
$$ select slug from public.accounts where name = 'Test' and slug = 'test-2' $$,
row('test-2'::text),
'The third team account should automatically create a slug named "test-2"'
);
-- Should automatically update the slug if the name is updated
update public.accounts set name = 'Test 4' where slug = 'test-2';
select row_eq(
$$ select slug from public.accounts where name = 'Test 4' $$,
row('test-4'::text),
'Updating the name of a team account should update the slug'
);
-- Should fail if the slug is updated to an existing slug
select throws_ok(
$$ update public.accounts set slug = 'test-1' where slug = 'test-4' $$,
'duplicate key value violates unique constraint "accounts_slug_key"'
);
-- Test special characters in the slug
update public.accounts set slug = 'test-5' where slug = 'test-4[';
select row_eq(
$$ select slug from public.accounts where name = 'Test 4' $$,
row('test-4'::text),
'Updating the name of a team account should update the slug'
);
-- Test various special characters
update public.accounts set name = 'Test@Special#Chars$' where slug = 'test-4';
select row_eq(
$$ select slug from public.accounts where name = 'Test@Special#Chars$' $$,
row('test-special-chars'::text),
'Special characters should be removed from slug'
);
-- Test multiple consecutive special characters
update public.accounts set name = 'Test!!Multiple---Special$$$Chars' where slug = 'test-special-chars';
select row_eq(
$a$ select slug from public.accounts where name = 'Test!!Multiple---Special$$$Chars' $a$,
row('test-multiple-special-chars'::text),
'Multiple consecutive special characters should be replaced with single hyphen'
);
-- Test leading and trailing special characters
update public.accounts set name = '!!!LeadingAndTrailing###' where slug = 'test-multiple-special-chars';
select row_eq(
$$ select slug from public.accounts where name = '!!!LeadingAndTrailing###' $$,
row('leadingandtrailing'::text),
'Leading and trailing special characters should be removed'
);
-- Test non-ASCII characters
update public.accounts set name = 'Testéñ中文Русский' where slug = 'leadingandtrailing';
select row_eq(
$$ select slug from public.accounts where name = 'Testéñ中文Русский' $$,
row('testen'::text),
'Non-ASCII characters should be transliterated or removed'
);
-- Test mixed case with special characters
update public.accounts set name = 'Test Mixed CASE With Special@Chars!' where slug = 'testen';
select row_eq(
$$ select slug from public.accounts where name = 'Test Mixed CASE With Special@Chars!' $$,
row('test-mixed-case-with-special-chars'::text),
'Mixed case should be converted to lowercase and special chars handled'
);
-- Test using parentheses
update public.accounts set name = 'Test (Parentheses)' where slug = 'test-mixed-case-with-special-chars';
select row_eq(
$$ select slug from public.accounts where name = 'Test (Parentheses)' $$,
row('test-parentheses'::text),
'Parentheses should be removed from slug'
);
-- Test using asterisk
update public.accounts set name = 'Test * Asterisk' where slug = 'test-parentheses';
select row_eq(
$$ select slug from public.accounts where name = 'Test * Asterisk' $$,
row('test-asterisk'::text),
'Asterisk should be removed from slug'
);
select * from finish();
ROLLBACK;

View File

@@ -1,94 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
-- another user not in the team
select tests.create_supabase_user('test', 'test@supabase.com');
-- an owner cannot remove the primary owner
select makerkit.authenticate_as('owner');
select throws_ok(
$$ delete from public.accounts_memberships
where account_id = makerkit.get_account_id_by_slug('makerkit')
and user_id = '31a03e74-1639-45b6-bfa7-77447f1a4762' $$,
'The primary account owner cannot be actioned'
);
-- an owner can remove accounts with lower roles
select lives_ok(
$$ delete from public.accounts_memberships
where account_id = makerkit.get_account_id_by_slug('makerkit')
and user_id = '6b83d656-e4ab-48e3-a062-c0c54a427368' $$,
'Owner should be able to remove a member'
);
-- a member cannot remove a member with a higher role
select makerkit.authenticate_as('member');
-- delete a membership record where the user is a higher role than the current user
select throws_ok(
$$ delete from public.accounts_memberships
where account_id = makerkit.get_account_id_by_slug('makerkit')
and user_id = '5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf' $$,
'You do not have permission to action a member from this account'
);
-- an primary_owner cannot remove themselves
select makerkit.authenticate_as('primary_owner');
select throws_ok(
$$ delete from public.accounts_memberships
where account_id = makerkit.get_account_id_by_slug('makerkit')
and user_id = '31a03e74-1639-45b6-bfa7-77447f1a4762' $$,
'The primary account owner cannot be removed from the account membership list'
);
-- a primary_owner can remove another member
select lives_ok(
$$ delete from public.accounts_memberships
where account_id = makerkit.get_account_id_by_slug('makerkit')
and user_id = 'b73eb03e-fb7a-424d-84ff-18e2791ce0b4'; $$,
'Primary owner should be able to remove another member'
);
-- foreigners
-- a user not in the account cannot remove a member
select makerkit.authenticate_as('test');
select throws_ok(
$$ delete from public.accounts_memberships
where account_id = '5deaa894-2094-4da3-b4fd-1fada0809d1c'
and user_id = tests.get_supabase_uid('owner'); $$,
'You do not have permission to action a member from this account'
);
select makerkit.authenticate_as('owner');
select isnt_empty(
$$ select 1 from public.accounts_memberships
where account_id = '5deaa894-2094-4da3-b4fd-1fada0809d1c'
and user_id = tests.get_supabase_uid('owner'); $$,
'Foreigners should not be able to remove members');
select makerkit.authenticate_as('test');
-- a user not in the account cannot remove themselves
select throws_ok(
$$ delete from public.accounts_memberships
where account_id = makerkit.get_account_id_by_slug('makerkit')
and user_id = auth.uid(); $$,
'You do not have permission to action a member from this account'
);
select * from finish();
rollback;

View File

@@ -1,111 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
-- test
select makerkit.set_identifier('test', 'test@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.authenticate_as('test');
select lives_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite1@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()); $$,
'owner should be able to create invitations'
);
-- check two invitations to the same email/account are not allowed
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite1@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()) $$,
'duplicate key value violates unique constraint "invitations_email_account_id_key"'
);
select makerkit.authenticate_as('member');
-- check a member cannot invite members with higher roles
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite2@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'owner', gen_random_uuid()) $$,
'new row violates row-level security policy for table "invitations"'
);
-- check a member can invite members with the same or lower roles
select lives_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite2@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()) $$,
'member should be able to create invitations for members or lower roles'
);
-- test invite exists
select isnt_empty(
$$ select * from public.invitations where account_id = makerkit.get_account_id_by_slug('makerkit') $$,
'invitations should be listed'
);
select makerkit.authenticate_as('owner');
-- check the owner can invite members with lower roles
select lives_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite3@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()) $$,
'owner should be able to create invitations'
);
-- authenticate_as the custom role
select makerkit.authenticate_as('custom');
-- it will fail because the custom role does not have the invites.manage permission
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite3@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'custom-role', gen_random_uuid()) $$,
'new row violates row-level security policy for table "invitations"'
);
set local role postgres;
-- add permissions to invite members to the custom role
insert into public.role_permissions (role, permission) values ('custom-role', 'invites.manage');
-- authenticate_as the custom role
select makerkit.authenticate_as('custom');
select lives_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite4@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'custom-role', gen_random_uuid()) $$,
'custom role should be able to create invitations'
);
select lives_ok(
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example@makerkit.dev', 'custom-role')::public.invitation]); $$,
'custom role should be able to create invitations using the function public.add_invitations_to_account'
);
select throws_ok(
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example2@makerkit.dev', 'owner')::public.invitation]); $$,
'new row violates row-level security policy for table "invitations"',
'cannot invite members with higher roles'
);
-- Foreigners should not be able to create invitations
select tests.create_supabase_user('user');
select makerkit.authenticate_as('user');
-- it will fail because the user is not a member of the account
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite4@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()) $$,
'new row violates row-level security policy for table "invitations"'
);
select throws_ok(
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example@example.com', 'member')::public.invitation]); $$,
'new row violates row-level security policy for table "invitations"'
);
select is_empty($$
select * from public.invitations where account_id = makerkit.get_account_id_by_slug('makerkit') $$,
'no invitations should be listed'
);
select * from finish();
rollback;

View File

@@ -1,92 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
-- another user not in the team
select tests.create_supabase_user('test', 'test@supabase.com');
select makerkit.authenticate_as('owner');
-- Can check if an account is a team member
-- Primary owner
select is(
(select public.is_team_member(
makerkit.get_account_id_by_slug('makerkit'),
tests.get_supabase_uid('member')
)),
true,
'The primary account owner can check if a member is a team member'
);
select makerkit.authenticate_as('member');
-- Member
select is(
(select public.is_team_member(
makerkit.get_account_id_by_slug('makerkit'),
tests.get_supabase_uid('owner')
)),
true,
'The member can check if another member is a team member'
);
select is(
(select public.has_role_on_account(
makerkit.get_account_id_by_slug('makerkit')
)),
true,
'The member can check if they have a role on the account'
);
select isnt_empty(
$$ select * from public.get_account_members('makerkit') $$,
'The member can query the team account memberships using the get_account_members function'
);
select makerkit.authenticate_as('test');
-- Foreigners
-- Cannot query the team account memberships
select is(
(select public.is_team_member(
makerkit.get_account_id_by_slug('makerkit'),
tests.get_supabase_uid('owner')
)),
false,
'The foreigner cannot check if a member is a team member'
);
-- Does not have a role on the account
select is(
(select public.has_role_on_account(
makerkit.get_account_id_by_slug('makerkit')
)),
false,
'The foreigner does not have a role on the account'
);
select is_empty(
$$ select * from public.accounts_memberships where account_id = makerkit.get_account_id_by_slug('makerkit') $$,
'The foreigner cannot query the team account memberships'
);
select is_empty(
$$ select * from public.accounts where id = makerkit.get_account_id_by_slug('makerkit') $$,
'The foreigner cannot query the team account'
);
select is_empty(
$$ select * from public.get_account_members('makerkit') $$,
'The foreigner cannot query the team members'
);
select * from finish();
rollback;

View File

@@ -1,77 +0,0 @@
BEGIN;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
--- we insert a user into auth.users and return the id into user_id to use
select tests.create_supabase_user('test1', 'test1@test.com');
select tests.create_supabase_user('test2');
select makerkit.authenticate_as('test1');
-- users cannot insert into notifications
select throws_ok(
$$ insert into public.notifications(account_id, body) values (tests.get_supabase_uid('test1'), 'test'); $$,
'permission denied for table notifications'
);
set local role service_role;
-- service role can insert into notifications
select lives_ok(
$$ insert into public.notifications(account_id, body) values (tests.get_supabase_uid('test1'), 'test'); $$,
'service role can insert into notifications'
);
select makerkit.authenticate_as('test1');
-- user can read their own notifications
select row_eq(
$$ select account_id, body from public.notifications where account_id = tests.get_supabase_uid('test1'); $$,
row (tests.get_supabase_uid('test1'), 'test'::varchar),
'user can read their own notifications'
);
-- user can read their team notifications
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
set local role service_role;
-- service role can insert into notifications
select lives_ok(
$$ insert into public.notifications(account_id, body) values (makerkit.get_account_id_by_slug('makerkit'), 'test'); $$,
'service role can insert into notifications'
);
select makerkit.authenticate_as('member');
select row_eq(
$$ select account_id, body from public.notifications where account_id = makerkit.get_account_id_by_slug('makerkit'); $$,
row (makerkit.get_account_id_by_slug('makerkit'), 'test'::varchar),
'user can read their team notifications'
);
-- foreigners
select makerkit.authenticate_as('test2');
-- foreigner cannot read other user's notifications
select is_empty(
$$ select account_id, body from public.notifications where account_id = tests.get_supabase_uid('test1'); $$,
'foreigner cannot read other users notifications'
);
-- foreigner cannot read other teams notifications
select is_empty(
$$ select account_id, body from public.notifications where account_id = makerkit.get_account_id_by_slug('makerkit'); $$,
'foreigner cannot read other teams notifications'
);
select * from finish();
rollback;

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
BEGIN;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
--- we insert a user into auth.users and return the id into user_id to use
select tests.create_supabase_user('test1', 'test1@test.com');
select tests.create_supabase_user('test2');
------------
--- Primary Owner
------------
select makerkit.authenticate_as('test1');
-- should create the personal account automatically with the same ID as the user
SELECT row_eq(
$$ select primary_owner_user_id, is_personal_account, name from public.accounts order by created_at desc limit 1 $$,
ROW (tests.get_supabase_uid('test1'), true, 'test1'::varchar),
'Inserting a user should create a personal account when personal accounts are enabled'
);
-- anon users should not be able to see the personal account
set local role anon;
SELECT throws_ok(
$$ select * from public.accounts order by created_at desc limit 1 $$,
'permission denied for schema public'
);
-- the primary owner should be able to see the personal account
select makerkit.authenticate_as('test1');
SELECT isnt_empty(
$$ select * from public.accounts where primary_owner_user_id = tests.get_supabase_uid('test1') $$,
'The primary owner should be able to see the personal account'
);
------------
--- Other Users
-- other users should not be able to see the personal account
select makerkit.authenticate_as('test2');
SELECT is_empty(
$$ select * from public.accounts where primary_owner_user_id = tests.get_supabase_uid('test1') $$,
'Other users should not be able to see the personal account'
);
SELECT *
FROM finish();
ROLLBACK;

View File

@@ -1,95 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
INSERT INTO public.billing_customers(account_id, provider, customer_id)
VALUES (tests.get_supabase_uid('primary_owner'), 'stripe', 'cus_test');
-- Call the upsert_order function
SELECT public.upsert_order(tests.get_supabase_uid('primary_owner'), 'cus_test', 'order_test', 'pending', 'stripe', 100, 'usd', '[
{"id":"order_item_1", "product_id": "prod_test", "variant_id": "var_test", "price_amount": 100, "quantity": 1},
{"id":"order_item_2", "product_id": "prod_test", "variant_id": "var_test_2", "price_amount": 100, "quantity": 10}
]');
-- Verify that the order was created correctly
SELECT is(
(SELECT status FROM public.orders WHERE id = 'order_test'),
'pending',
'The order status should be pending'
);
-- Verify that the subscription items were created correctly
SELECT row_eq(
$$ select count(*) from order_items where order_id = 'order_test' $$,
row(2::bigint),
'The order items should be created'
);
-- Call the upsert_order function again to update the order
select public.upsert_order(tests.get_supabase_uid('primary_owner'), 'cus_test', 'order_test', 'succeeded', 'stripe', 100, 'usd', '[
{"id":"order_item_1", "product_id": "prod_test_2", "variant_id": "var_test", "price_amount": 100, "quantity": 10}
]');
-- Verify that the order was updated correctly
select is(
(select status FROM public.orders WHERE id = 'order_test'),
'succeeded',
'The order status should be succeeded'
);
select row_eq(
$$ select quantity from order_items where variant_id = 'var_test' $$,
row(10::int),
'The order items should be updated'
);
select is_empty(
$$ select * from order_items where id = 'order_item_2' $$,
'The order item should be deleted when the order is updated'
);
select row_eq(
$$ select product_id from order_items where id = 'order_item_1' $$,
row('prod_test_2'::text),
'The order item should be deleted when the order is updated'
);
select makerkit.authenticate_as('primary_owner');
-- account can read their own subscription
select isnt_empty(
$$ select 1 from orders where id = 'order_test' $$,
'The account can read their own order'
);
select isnt_empty(
$$ select * from order_items where order_id = 'order_test' $$,
'The account can read their own orders items'
);
-- foreigners
select tests.create_supabase_user('foreigner');
select makerkit.authenticate_as('foreigner');
-- account cannot read other's subscription
select is_empty(
$$ select 1 from orders where id = 'order_test' $$,
'The account cannot read the other account orders'
);
select is_empty(
$$ select 1 from order_items where order_id = 'order_test' $$,
'The account cannot read the other account order items'
);
-- Finish the tests and clean up
select * from finish();
rollback;

View File

@@ -1,196 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
-- Create a test account and billing customer
INSERT INTO public.billing_customers(account_id, provider, customer_id)
VALUES (tests.get_supabase_uid('primary_owner'), 'stripe', 'cus_test');
-- Call the upsert_subscription function
SELECT public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_test', 'sub_test', true, 'active', 'stripe', false, 'usd', now(), now() + interval '1 month', '[
{
"id": "sub_123",
"product_id": "prod_test",
"variant_id": "var_test",
"type": "flat",
"price_amount": 1000,
"quantity": 1,
"interval": "month",
"interval_count": 1
},
{
"id": "sub_456",
"product_id": "prod_test_2",
"variant_id": "var_test_2",
"type": "flat",
"price_amount": 2000,
"quantity": 2,
"interval": "month",
"interval_count": 1
},
{
"id": "sub_789",
"product_id": "prod_test_3",
"variant_id": "var_test_3",
"type": "flat",
"price_amount": 2000,
"quantity": 2,
"interval": "month",
"interval_count": 1
}
]');
-- Verify that the subscription items were created correctly
SELECT row_eq(
$$ select count(*) from subscription_items where subscription_id = 'sub_test' $$,
row(3::bigint),
'The subscription items should be created'
);
-- Verify that the subscription was created correctly
SELECT is(
(SELECT active FROM public.subscriptions WHERE id = 'sub_test'),
true,
'The subscription should be active'
);
SELECT is(
(SELECT status FROM public.subscriptions WHERE id = 'sub_test'),
'active',
'The subscription status should be active'
);
-- Call the upsert_subscription function again to update the subscription
SELECT public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_test', 'sub_test', false, 'past_due', 'stripe', true, 'usd', now(), now() + interval '1 month', '[
{
"id": "sub_123",
"product_id": "prod_test",
"variant_id": "var_test",
"type": "flat",
"price_amount": 2000,
"quantity": 1,
"interval": "month",
"interval_count": 1
},
{
"id": "sub_456",
"product_id": "prod_test_3",
"variant_id": "var_test_2",
"type": "flat",
"price_amount": 2000,
"quantity": 2,
"interval": "year",
"interval_count": 12
}
]');
-- Verify that the subscription items were updated correctly
SELECT row_eq(
$$ select price_amount from subscription_items where variant_id = 'var_test' $$,
row('2000'::numeric),
'The subscription items should be updated'
);
-- Verify that the subscription items were updated correctly
SELECT row_eq(
$$ select product_id from subscription_items where id = 'sub_456' $$,
row('prod_test_3'::varchar),
'The subscription items should be updated'
);
-- Verify that the subscription items were updated correctly
SELECT row_eq(
$$ select interval from subscription_items where variant_id = 'var_test_2' $$,
row('year'::varchar),
'The subscription items should be updated'
);
-- Verify that the subscription was updated correctly
select is(
(select active FROM public.subscriptions WHERE id = 'sub_test'),
false,
'The subscription should be inactive'
);
select is(
(select status FROM public.subscriptions WHERE id = 'sub_test'),
'past_due',
'The subscription status should be past_due'
);
select isnt_empty(
$$ select * from public.subscription_items where subscription_id = 'sub_test' $$,
'The account can read their own subscription items'
);
select is_empty(
$$ select * from public.subscription_items where subscription_id = 'sub_test' and variant_id = 'var_test_3' $$,
'The subscription items should be deleted when the subscription is updated and the item is missing'
);
-- Call the upsert_subscription function again to update the subscription
select public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_test', 'sub_test', true, 'active', 'stripe', false, 'usd', now(), now() + interval '1 month', '[]');
-- Verify that the subscription was updated correctly
select is(
(select active FROM public.subscriptions WHERE id = 'sub_test'),
true,
'The subscription should be active'
);
select makerkit.authenticate_as('primary_owner');
-- account can read their own subscription
select isnt_empty(
$$ select 1 from subscriptions where id = 'sub_test' $$,
'The account can read their own subscription'
);
select is_empty(
$$ select * from subscription_items where subscription_id = 'sub_test' $$,
'No subscription items should be returned when the subscription is empty'
);
-- users cannot manually update subscriptions
select throws_ok(
$$ select public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_test', 'sub_test', true, 'active', 'stripe', false, 'usd', now(), now() + interval '1 month', '[]') $$,
'permission denied for function upsert_subscription'
);
select is(
(public.has_active_subscription(tests.get_supabase_uid('primary_owner'))),
true,
'The function public.has_active_subscription should return true when the account has a subscription'
);
-- foreigners
select tests.create_supabase_user('foreigner');
select makerkit.authenticate_as('foreigner');
-- account cannot read other's subscription
select is_empty(
$$ select 1 from subscriptions where id = 'sub_test' $$,
'The account cannot read the other account subscriptions'
);
select is_empty(
$$ select 1 from subscription_items where subscription_id = 'sub_test' $$,
'The account cannot read the other account subscription items'
);
select is(
(public.has_active_subscription(tests.get_supabase_uid('primary_owner'))),
false,
'The function public.has_active_subscription should return false when a foreigner is querying the account subscription'
);
-- Finish the tests and clean up
select * from finish();
rollback;

View File

@@ -1,57 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select
no_plan();
CREATE OR REPLACE FUNCTION check_schema_conditions()
RETURNS void AS
$$
DECLARE
_table RECORD;
_column RECORD;
columnCheckCount INTEGER;
BEGIN
FOR _table IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public')
LOOP
-- 1. Check if every table has RLS enabled
IF (
SELECT relrowsecurity FROM pg_class
INNER JOIN pg_namespace n ON n.oid = pg_class.relnamespace
WHERE n.nspname = 'public' AND relname = _table.tablename
) IS FALSE THEN
RAISE EXCEPTION 'Table "%" does not have RLS enabled.', _table.tablename;
END IF;
-- 2. Check that every text column in the current table has a constraint
FOR _column IN (SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = _table.tablename AND data_type = 'text')
LOOP
SELECT COUNT(*)
INTO columnCheckCount
FROM information_schema.constraint_column_usage
WHERE table_schema = 'public' AND table_name = _table.tablename AND column_name = _column.column_name;
IF columnCheckCount = 0 THEN
RAISE NOTICE 'Text column "%.%" does not have a constraint
.',
_table.tablename, _column.column_name;
END IF;
END LOOP;
END LOOP;
RAISE NOTICE 'Schema check completed.';
END
$$ LANGUAGE plpgsql;
select lives_ok($$
select
check_schema_conditions();
$$, 'check_schema_conditions()');
select
*
from
finish();
rollback;

View File

@@ -1,52 +0,0 @@
BEGIN;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select has_table('public', 'config', 'Makerkit config table should exist');
select has_table('public', 'accounts', 'Makerkit accounts table should exist');
select has_table('public', 'accounts_memberships', 'Makerkit account_users table should exist');
select has_table('public', 'invitations', 'Makerkit invitations table should exist');
select has_table('public', 'billing_customers', 'Makerkit billing_customers table should exist');
select has_table('public', 'subscriptions', 'Makerkit subscriptions table should exist');
select has_table('public', 'subscription_items', 'Makerkit subscription_items table should exist');
select has_table('public', 'orders', 'Makerkit orders table should exist');
select has_table('public', 'order_items', 'Makerkit order_items table should exist');
select has_table('public', 'roles', 'Makerkit roles table should exist');
select has_table('public', 'role_permissions', 'Makerkit roles_permissions table should exist');
select tests.rls_enabled('public', 'config');
select tests.rls_enabled('public', 'accounts');
select tests.rls_enabled('public', 'accounts_memberships');
select tests.rls_enabled('public', 'invitations');
select tests.rls_enabled('public', 'billing_customers');
select tests.rls_enabled('public', 'subscriptions');
select tests.rls_enabled('public', 'subscription_items');
select tests.rls_enabled('public', 'orders');
select tests.rls_enabled('public', 'order_items');
select tests.rls_enabled('public', 'roles');
select tests.rls_enabled('public', 'role_permissions');
SELECT schema_privs_are('public', 'anon', Array [NULL], 'Anon should not have access to public schema');
-- set the role to anonymous for verifying access tests
set role anon;
select throws_ok('select public.get_config()');
select throws_ok('select public.is_set(''enable_team_accounts'')');
-- set the role to the service_role for testing access
set role service_role;
select ok(public.get_config() is not null),
'Makerkit get_config should be accessible to the service role';
-- set the role to authenticated for tests
set role authenticated;
select ok(public.get_config() is not null), 'Makerkit get_config should be accessible to authenticated users';
select ok(public.is_set('enable_team_accounts')),
'Makerkit is_set should be accessible to authenticated users';
select isnt_empty('select * from public.config', 'authenticated users should have access to Makerkit config');
SELECT *
FROM finish();
ROLLBACK;

View File

@@ -1,122 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
select makerkit.authenticate_as('member');
select throws_ok(
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
('account_image', '{"key": "value"}', tests.get_supabase_uid('primary_owner'), tests.get_supabase_uid('primary_owner'), tests.get_supabase_uid('primary_owner'), 1); $$,
'new row violates row-level security policy for table "objects"'
);
select makerkit.authenticate_as('primary_owner');
select lives_ok(
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
('account_image', '{"key": "value"}', tests.get_supabase_uid('primary_owner'), tests.get_supabase_uid('primary_owner'), tests.get_supabase_uid('primary_owner'), 1); $$,
'The owner should be able to insert a new object'
);
select isnt_empty(
$$ select * from storage.objects where owner = tests.get_supabase_uid('primary_owner') $$,
'The object should be inserted'
);
select makerkit.authenticate_as('owner');
select is_empty(
$$ select * from storage.objects where owner = tests.get_supabase_uid('primary_owner') $$,
'The owner should not be able to see the object'
);
-- create a new bucket
--
set local role postgres;
select lives_ok(
$$ insert into storage.buckets ("name", "id", public) values ('new_bucket', 'new_bucket', true); $$
);
-- we create a mock policy allowing only the primary_owner to access the new bucket
-- this is a mock policy to check the existing policy system does not interfere with the new bucket
create policy new_bucket_policy on storage.objects for all using (
bucket_id = 'new_bucket'
and auth.uid() = tests.get_supabase_uid('primary_owner')
)
with check (
bucket_id = 'new_bucket'
and auth.uid() = tests.get_supabase_uid('primary_owner')
);
select makerkit.authenticate_as('member');
-- user should not be able to insert into the new bucket according to the new policy
select throws_ok(
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
('new_bucket', '{"key": "value"}', 'some name', tests.get_supabase_uid('primary_owner'), tests.get_supabase_uid('primary_owner'), 1); $$,
'new row violates row-level security policy for table "objects"'
);
select makerkit.authenticate_as('primary_owner');
-- primary_owner should be able to insert into the new bucket according to the new policy
-- this is to check the new policy system is working
--
select lives_ok(
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
('new_bucket', '{"key": "value"}', 'some name', tests.get_supabase_uid('primary_owner'), tests.get_supabase_uid('primary_owner'), 1); $$,
'new row violates row-level security policy for table "objects"'
);
set local role postgres;
-- create a new bucket with a custom policy
--
create policy new_custom_bucket_policy on storage.objects for all using (
bucket_id = 'new_bucket'
and auth.uid() = tests.get_supabase_uid('owner')
)
with check (
bucket_id = 'new_bucket'
and auth.uid() = tests.get_supabase_uid('owner')
);
select makerkit.authenticate_as('owner');
-- insert a new object into the new bucket
--
select lives_ok(
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
('new_bucket', '{"key": "value"}', 'some name 2', tests.get_supabase_uid('primary_owner'), tests.get_supabase_uid('primary_owner'), 1); $$,
'The primary_owner should be able to insert a new object into the new bucket'
);
-- check the object is inserted
--
select isnt_empty(
$$ select * from storage.objects where bucket_id = 'new_bucket' $$,
'The object should be inserted into the new bucket'
);
-- check other members cannot insert into the new bucket
select makerkit.authenticate_as('member');
select throws_ok(
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
('new_bucket', '{"key": "value"}', 'some other name', tests.get_supabase_uid('primary_owner'), tests.get_supabase_uid('primary_owner'), 1); $$,
'new row violates row-level security policy for table "objects"'
);
select
*
from
finish();
rollback;

View File

@@ -1,84 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
-- Create test users for different scenarios
select tests.create_supabase_user('transitioning_admin');
select tests.create_supabase_user('revoking_mfa_admin');
select tests.create_supabase_user('concurrent_session_user');
-- Set up test users
select makerkit.set_identifier('transitioning_admin', 'transitioning@makerkit.dev');
select makerkit.set_identifier('revoking_mfa_admin', 'revoking@makerkit.dev');
select makerkit.set_identifier('concurrent_session_user', 'concurrent@makerkit.dev');
-- Test 1: Role Transition Scenarios
select makerkit.authenticate_as('transitioning_admin');
select makerkit.set_mfa_factor();
select makerkit.set_session_aal('aal2');
-- Initially not a super admin
select is(
(select public.is_super_admin()),
false,
'User should not be super admin initially'
);
-- Grant super admin
select makerkit.set_super_admin();
select is(
(select public.is_super_admin()),
true,
'User should now be super admin'
);
-- Test 2: MFA Revocation Scenarios
select makerkit.authenticate_as('revoking_mfa_admin');
select makerkit.set_mfa_factor();
select makerkit.set_session_aal('aal2');
select makerkit.set_super_admin();
-- Initially has super admin access
select is(
(select public.is_super_admin()),
true,
'Admin should have super admin access initially'
);
-- Simulate MFA revocation by setting AAL1
select makerkit.set_session_aal('aal1');
select is(
(select public.is_super_admin()),
false,
'Admin should lose super admin access when MFA is revoked'
);
-- Test 3: Concurrent Session Management
select makerkit.authenticate_as('concurrent_session_user');
select makerkit.set_mfa_factor();
select makerkit.set_session_aal('aal2');
select makerkit.set_super_admin();
-- Test access with AAL2
select is(
(select public.is_super_admin()),
true,
'Should have super admin access with AAL2'
);
-- Simulate different session with AAL1
select makerkit.set_session_aal('aal1');
select is(
(select public.is_super_admin()),
false,
'Should not have super admin access with AAL1 even if other session has AAL2'
);
-- Finish the tests and clean up
select * from finish();
rollback;

View File

@@ -1,210 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
-- Create Users
select tests.create_supabase_user('super_admin');
select tests.create_supabase_user('regular_user');
select tests.create_supabase_user('mfa_user');
select tests.create_supabase_user('malicious_user');
select tests.create_supabase_user('partial_mfa_user');
-- Set up test users
select makerkit.set_identifier('super_admin', 'super@makerkit.dev');
select makerkit.set_identifier('regular_user', 'regular@makerkit.dev');
select makerkit.set_identifier('mfa_user', 'mfa@makerkit.dev');
select makerkit.set_identifier('malicious_user', 'malicious@makerkit.dev');
select makerkit.set_identifier('partial_mfa_user', 'partial@makerkit.dev');
-- Test is_aal2 function
set local role postgres;
create or replace function makerkit.setup_super_admin() returns void as $$
begin
perform makerkit.authenticate_as('super_admin');
perform makerkit.set_mfa_factor();
perform makerkit.set_session_aal('aal2');
perform makerkit.set_super_admin();
end $$ language plpgsql;
-- Test super admin with AAL2
select makerkit.setup_super_admin();
select is(
(select public.is_aal2()),
true,
'Super admin should have AAL2 authentication'
);
select is(
(select public.is_super_admin()),
true,
'User should be identified as super admin'
);
-- Test regular user (no AAL2)
select makerkit.authenticate_as('regular_user');
select is(
(select public.is_aal2()),
false,
'Regular user should not have AAL2 authentication'
);
select is(
(select public.is_super_admin()),
false,
'Regular user should not be identified as super admin'
);
-- Test MFA compliance
set local role postgres;
select is(
(select public.is_super_admin()),
false,
'Postgres user should not be identified as super admin'
);
select makerkit.authenticate_as('mfa_user');
select makerkit.set_mfa_factor();
select makerkit.set_session_aal('aal2');
select is(
(select public.is_mfa_compliant()),
true,
'User with verified MFA should be MFA compliant because it is optional'
);
-- Test super admin access to protected tables
select makerkit.setup_super_admin();
-- Test malicious user attempts
select makerkit.authenticate_as('malicious_user');
-- Attempt to fake super admin role (should fail)
select is(
(select public.is_super_admin()),
false,
'Malicious user cannot fake super admin role'
);
-- Test access to protected tables (should be restricted)
select is_empty(
$$ select * from public.accounts where id != auth.uid() $$,
'Malicious user should not access other accounts'
);
select is_empty(
$$ select * from public.accounts_memberships where user_id != auth.uid() $$,
'Malicious user should not access other memberships'
);
select is_empty(
$$ select * from public.subscriptions where account_id != auth.uid() $$,
'Malicious user should not access other subscriptions'
);
-- Test partial MFA setup (not verified)
select makerkit.authenticate_as('partial_mfa_user');
select makerkit.set_session_aal('aal2');
-- Test regular user restricted access
select makerkit.authenticate_as('regular_user');
-- Test MFA restrictions
select makerkit.authenticate_as('regular_user');
select makerkit.set_mfa_factor();
-- Should be restricted without MFA
select is_empty(
$$ select * from public.accounts $$,
'Regular user without MFA should not access accounts when MFA is required'
);
-- A super admin without MFA should not be able to have super admin rights
select makerkit.authenticate_as('super_admin');
select makerkit.set_super_admin();
select is(
(select public.is_super_admin()),
false,
'Super admin without MFA should not be able to have super admin rights'
);
-- Test edge cases for MFA and AAL2
select makerkit.authenticate_as('mfa_user');
select makerkit.set_mfa_factor();
-- Set AAL1 despite having MFA to test edge case
select makerkit.set_session_aal('aal1');
select is(
(select public.is_mfa_compliant()),
false,
'User with MFA but AAL1 session should not be MFA compliant'
);
select is_empty(
$$ select * from public.accounts $$,
'Non-compliant MFA should not be able to read any accounts'
);
select is_empty(
$$ select * from public.accounts_memberships $$,
'Non-compliant MFA should not be able to read any memberships'
);
-- A Super Admin should be able to access all tables when MFA is enabled
select makerkit.setup_super_admin();
select is(
(select public.is_super_admin()),
true,
'Super admin has super admin rights'
);
-- Test comprehensive access for super admin
select isnt_empty(
$$ select * from public.accounts where id = tests.get_supabase_uid('regular_user') $$,
'Super admin should be able to access all accounts'
);
do $$
begin
delete from public.accounts where id = tests.get_supabase_uid('regular_user');
end $$;
-- A Super admin cannot delete accounts directly
select isnt_empty(
$$ select * from public.accounts where id = tests.get_supabase_uid('regular_user') $$,
'Super admin should not be able to delete data directly'
);
set local role postgres;
-- update the account name to be able to test the update
do $$
begin
update public.accounts set name = 'Regular User' where id = tests.get_supabase_uid('regular_user');
end $$;
-- re-authenticate as super admin
select makerkit.setup_super_admin();
-- test a super admin cannot update accounts directly
do $$
begin
update public.accounts set name = 'Super Admin' where id = tests.get_supabase_uid('regular_user');
end $$;
select row_eq(
$$ select name from public.accounts where id = tests.get_supabase_uid('regular_user') $$,
row('Regular User'::varchar),
'Super admin should not be able to update data directly'
);
-- Finish the tests and clean up
select * from finish();
rollback;

View File

@@ -1,775 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select
no_plan();
--- we insert a user into auth.users and return the id into user_id to use
select
tests.create_supabase_user('test1', 'test1@test.com');
select
tests.create_supabase_user('test2');
-- Create an team account
select
makerkit.authenticate_as('test1');
select
public.create_team_account('Test');
select
row_eq($$
select
primary_owner_user_id, is_personal_account, slug, name
from makerkit.get_account_by_slug('test') $$,
row (tests.get_supabase_uid('test1'), false,
'test'::text, 'Test'::varchar),
'Users can create a team account');
-- Should be the primary owner of the team account by default
select
row_eq($$
select
account_role from public.accounts_memberships
where
account_id =(
select
id
from public.accounts
where
slug = 'test')
and user_id = tests.get_supabase_uid('test1')
$$, row ('owner'::varchar),
'The primary owner should have the owner role for the team account');
select is(
public.is_account_owner((select
id
from public.accounts
where
slug = 'test')),
true,
'The current user should be the owner of the team account'
);
-- Should be able to see the team account
select
isnt_empty($$
select
* from public.accounts
where
primary_owner_user_id =
tests.get_supabase_uid('test1') $$,
'The primary owner should be able to see the team account');
-- Others should not be able to see the team account
select
makerkit.authenticate_as('test2');
select is(
public.is_account_owner((select
id
from public.accounts
where
slug = 'test')),
false,
'The current user should not be the owner of the team account'
);
select
is_empty($$
select
* from public.accounts
where
primary_owner_user_id =
tests.get_supabase_uid('test1') $$,
'Other users should not be able to see the team account');
-- should not have any role for the team account
select
is (public.has_role_on_account((
select
id
from makerkit.get_account_by_slug('test'))),
false,
'Foreign users should not have any role for the team account');
-- enforcing a single team account per owner using a trigger when
-- inserting a team
set local role postgres;
create or replace function kit.single_account_per_owner()
returns trigger
as $$
declare
total_accounts int;
begin
select
count(id)
from
public.accounts
where
primary_owner_user_id = auth.uid() into total_accounts;
if total_accounts > 0 then
raise exception 'User can only own 1 account';
end if;
return NEW;
end
$$
language plpgsql
set search_path = '';
-- trigger to protect account fields
create trigger single_account_per_owner
before insert on public.accounts for each row
execute function kit.single_account_per_owner();
-- Create an team account
select
makerkit.authenticate_as('test1');
select
throws_ok(
$$ select
public.create_team_account('Test2') $$, 'User can only own 1 account');
set local role postgres;
drop trigger single_account_per_owner on public.accounts;
-- Test that a member cannot update another account in the same team
-- Using completely new users for update tests
select
tests.create_supabase_user('updatetest1', 'updatetest1@test.com');
select
tests.create_supabase_user('updatetest2', 'updatetest2@test.com');
-- Create a team account for update tests
select
makerkit.authenticate_as('updatetest1');
select
public.create_team_account('UpdateTeam');
-- Add updatetest2 as a member
set local role postgres;
insert into public.accounts_memberships (account_id, user_id, account_role)
values (
(select id from makerkit.get_account_by_slug('updateteam')),
tests.get_supabase_uid('updatetest2'),
'member'
);
-- Verify updatetest2 is now a member
select
makerkit.authenticate_as('updatetest1');
select
row_eq($$
select
account_role from public.accounts_memberships
where
account_id = (select id from makerkit.get_account_by_slug('updateteam'))
and user_id = tests.get_supabase_uid('updatetest2')
$$,
row ('member'::varchar),
'updatetest2 should be a member of the team account'
);
-- Store original values to verify they don't change
select
row_eq($$
select name, primary_owner_user_id from public.accounts
where id = (select id from makerkit.get_account_by_slug('updateteam'))
$$,
row ('UpdateTeam'::varchar, tests.get_supabase_uid('updatetest1')),
'Original values before attempted updates'
);
-- Add team account to updatetest2's visibility (so they can try to perform operations)
select
makerkit.authenticate_as('updatetest2');
-- First verify that as a member, updatetest2 can now see the account
select
isnt_empty($$
select
* from public.accounts
where id = (select id from makerkit.get_account_by_slug('updateteam'))
$$,
'Team member should be able to see the team account'
);
-- Try to update the team name - without checking for exception
select
lives_ok($$
update public.accounts
set name = 'Updated Team Name'
where id = (select id from makerkit.get_account_by_slug('updateteam'))
$$,
'Non-owner member update attempt should not crash'
);
-- Try to update primary owner without checking for exception
select
lives_ok($$
update public.accounts
set primary_owner_user_id = tests.get_supabase_uid('updatetest2')
where id = (select id from makerkit.get_account_by_slug('updateteam'))
$$,
'Non-owner member update of primary owner attempt should not crash'
);
-- Verify the values have not changed by checking in both updatetest1 and updatetest2 sessions
-- First check as updatetest2 (the member)
select
row_eq($$
select name, primary_owner_user_id from public.accounts
where id = (select id from makerkit.get_account_by_slug('updateteam'))
$$,
row ('UpdateTeam'::varchar, tests.get_supabase_uid('updatetest1')),
'Values should remain unchanged after member update attempt (member perspective)'
);
-- Now verify as updatetest1 (the owner)
select
makerkit.authenticate_as('updatetest1');
select
row_eq($$
select name, primary_owner_user_id from public.accounts
where id = (select id from makerkit.get_account_by_slug('updateteam'))
$$,
row ('UpdateTeam'::varchar, tests.get_supabase_uid('updatetest1')),
'Values should remain unchanged after member update attempt (owner perspective)'
);
-- Test role escalation prevention with completely new users
select
tests.create_supabase_user('roletest1', 'roletest1@test.com');
select
tests.create_supabase_user('roletest2', 'roletest2@test.com');
-- Create a team account for role tests
select
makerkit.authenticate_as('roletest1');
select
public.create_team_account('RoleTeam');
-- Add roletest2 as a member
set local role postgres;
insert into public.accounts_memberships (account_id, user_id, account_role)
values (
(select id from makerkit.get_account_by_slug('roleteam')),
tests.get_supabase_uid('roletest2'),
'member'
);
-- Test role escalation prevention: a member cannot promote themselves to owner
select
makerkit.authenticate_as('roletest2');
-- Try to update own role to owner
select
lives_ok($$
update public.accounts_memberships
set account_role = 'owner'
where account_id = (select id from makerkit.get_account_by_slug('roleteam'))
and user_id = tests.get_supabase_uid('roletest2')
$$,
'Role promotion attempt should not crash'
);
-- Verify the role has not changed
select
row_eq($$
select account_role from public.accounts_memberships
where account_id = (select id from makerkit.get_account_by_slug('roleteam'))
and user_id = tests.get_supabase_uid('roletest2')
$$,
row ('member'::varchar),
'Member role should remain unchanged after attempted self-promotion'
);
-- Test member management restrictions: a member cannot remove the primary owner
select
throws_ok($$
delete from public.accounts_memberships
where account_id = (select id from makerkit.get_account_by_slug('roleteam'))
and user_id = tests.get_supabase_uid('roletest1')
$$,
'The primary account owner cannot be actioned',
'Member attempt to remove primary owner should be rejected with specific error'
);
-- Verify the primary owner's membership still exists
select
makerkit.authenticate_as('roletest1');
select
isnt_empty($$
select * from public.accounts_memberships
where account_id = (select id from makerkit.get_account_by_slug('roleteam'))
and user_id = tests.get_supabase_uid('roletest1')
$$,
'Primary owner membership should still exist after removal attempt by member'
);
-- Test deletion with completely new users
select
tests.create_supabase_user('deletetest1', 'deletetest1@test.com');
select
tests.create_supabase_user('deletetest2', 'deletetest2@test.com');
-- Create a team account for delete tests
select
makerkit.authenticate_as('deletetest1');
select
public.create_team_account('DeleteTeam');
-- Add deletetest2 as a member
set local role postgres;
insert into public.accounts_memberships (account_id, user_id, account_role)
values (
(select id from makerkit.get_account_by_slug('deleteteam')),
tests.get_supabase_uid('deletetest2'),
'member'
);
-- Test Delete Team Account
select
makerkit.authenticate_as('deletetest2');
-- deletion don't throw an error
select lives_ok(
$$ delete from public.accounts where id = (select id from makerkit.get_account_by_slug('deleteteam')) $$,
'Non-owner member deletion attempt should not crash'
);
select makerkit.authenticate_as('deletetest1');
select isnt_empty(
$$ select * from public.accounts where id = (select id from makerkit.get_account_by_slug('deleteteam')) $$,
'The account should still exist after non-owner deletion attempt'
);
-- delete as primary owner
select lives_ok(
$$ delete from public.accounts where id = (select id from makerkit.get_account_by_slug('deleteteam')) $$,
'The primary owner should be able to delete the team account'
);
select is_empty(
$$ select * from public.accounts where id = (select id from makerkit.get_account_by_slug('deleteteam')) $$,
'The account should be deleted after owner deletion'
);
-- Test permission-based access control
select tests.create_supabase_user('permtest1', 'permtest1@test.com');
select tests.create_supabase_user('permtest2', 'permtest2@test.com');
select tests.create_supabase_user('permtest3', 'permtest3@test.com');
-- Create a team account for permission tests
select makerkit.authenticate_as('permtest1');
select public.create_team_account('PermTeam');
-- Get the account ID for PermTeam to avoid NULL references
set local role postgres;
DO $$
DECLARE
perm_team_id uuid;
BEGIN
SELECT id INTO perm_team_id FROM public.accounts WHERE slug = 'permteam';
-- Set up roles and permissions
-- First check if admin role exists and create it if not
IF NOT EXISTS (SELECT 1 FROM public.roles WHERE name = 'admin') THEN
INSERT INTO public.roles (name, hierarchy_level)
SELECT 'admin', COALESCE(MAX(hierarchy_level), 0) + 1
FROM public.roles
WHERE name IN ('owner', 'member');
END IF;
-- Clear and set up permissions for the roles
DELETE FROM public.role_permissions WHERE role IN ('owner', 'admin', 'member');
INSERT INTO public.role_permissions (role, permission) VALUES
('owner', 'members.manage'),
('owner', 'invites.manage'),
('owner', 'roles.manage'),
('owner', 'billing.manage'),
('owner', 'settings.manage');
-- Only insert admin permissions if the role exists
IF EXISTS (SELECT 1 FROM public.roles WHERE name = 'admin') THEN
INSERT INTO public.role_permissions (role, permission) VALUES
('admin', 'members.manage'),
('admin', 'invites.manage');
END IF;
-- Add permtest2 as admin and permtest3 as member
-- Use explicit account_id to avoid NULL issues
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
VALUES (perm_team_id, tests.get_supabase_uid('permtest2'), 'admin');
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
VALUES (perm_team_id, tests.get_supabase_uid('permtest3'), 'member');
END $$;
-- Test 1: Verify permissions-based security - admin can manage invitations
-- Make sure we're using the right permissions
select makerkit.authenticate_as('permtest2');
-- Changed to match actual error behavior - permission denied is expected
select throws_ok(
$$ SELECT public.create_invitation(
(SELECT id FROM public.accounts WHERE slug = 'permteam'),
'test_invite@example.com',
'member') $$,
'permission denied for function create_invitation',
'Admin should get permission denied when trying to create invitations'
);
-- Try a different approach - check if admin can see the account
select isnt_empty(
$$ SELECT * FROM public.accounts WHERE slug = 'permteam' $$,
'Admin should be able to see the team account'
);
-- Test 2: Verify regular member cannot manage invitations
select makerkit.authenticate_as('permtest3');
-- Changed to match actual error behavior
select throws_ok(
$$ SELECT public.create_invitation(
(SELECT id FROM public.accounts WHERE slug = 'permteam'),
'test_invite@example.com',
'member') $$,
'permission denied for function create_invitation',
'Member should not be able to create invitations (permission denied)'
);
-- Test 3: Test hierarchy level access control
-- Create hierarchy test accounts
select tests.create_supabase_user('hiertest1', 'hiertest1@test.com');
select tests.create_supabase_user('hiertest2', 'hiertest2@test.com');
select tests.create_supabase_user('hiertest3', 'hiertest3@test.com');
select tests.create_supabase_user('hiertest4', 'hiertest4@test.com');
-- Create a team account for hierarchy tests
select makerkit.authenticate_as('hiertest1');
select public.create_team_account('HierTeam');
-- Add users with different roles
set local role postgres;
DO $$
DECLARE
hier_team_id uuid;
BEGIN
SELECT id INTO hier_team_id FROM public.accounts WHERE slug = 'hierteam';
-- Add users with different roles using explicit account_id
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
VALUES (hier_team_id, tests.get_supabase_uid('hiertest2'), 'admin');
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
VALUES (hier_team_id, tests.get_supabase_uid('hiertest3'), 'member');
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
VALUES (hier_team_id, tests.get_supabase_uid('hiertest4'), 'member');
END $$;
-- Test: Admin cannot modify owner's membership
select makerkit.authenticate_as('hiertest2');
select throws_ok(
$$ DELETE FROM public.accounts_memberships
WHERE account_id = (SELECT id FROM public.accounts WHERE slug = 'hierteam')
AND user_id = tests.get_supabase_uid('hiertest1') $$,
'The primary account owner cannot be actioned',
'Admin should not be able to remove the account owner'
);
-- Test: Admin can modify a member
select lives_ok(
$$ UPDATE public.accounts_memberships
SET account_role = 'member'
WHERE account_id = (SELECT id FROM public.accounts WHERE slug = 'hierteam')
AND user_id = tests.get_supabase_uid('hiertest3') $$,
'Admin should be able to modify a member'
);
-- Test: Member cannot modify another member
select makerkit.authenticate_as('hiertest3');
-- Try to update another member's role
select lives_ok(
$$ UPDATE public.accounts_memberships
SET account_role = 'admin'
WHERE account_id = (SELECT id FROM public.accounts WHERE slug = 'hierteam')
AND user_id = tests.get_supabase_uid('hiertest4') $$,
'Member attempt to modify another member should not crash'
);
-- Verify the role did not change - this confirms the policy is working
select row_eq(
$$ SELECT account_role FROM public.accounts_memberships
WHERE account_id = (SELECT id FROM public.accounts WHERE slug = 'hierteam')
AND user_id = tests.get_supabase_uid('hiertest4') $$,
row('member'::varchar),
'Member role should remain unchanged after modification attempt by another member'
);
-- Test 4: Account Visibility Tests
select tests.create_supabase_user('vistest1', 'vistest1@test.com');
select tests.create_supabase_user('vistest2', 'vistest2@test.com');
select tests.create_supabase_user('vistest3', 'vistest3@test.com');
-- Create a team account
select makerkit.authenticate_as('vistest1');
select public.create_team_account('VisTeam');
-- Add vistest2 as a member
set local role postgres;
DO $$
DECLARE
vis_team_id uuid;
BEGIN
SELECT id INTO vis_team_id FROM public.accounts WHERE slug = 'visteam';
-- Add member with explicit account_id
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
VALUES (vis_team_id, tests.get_supabase_uid('vistest2'), 'member');
END $$;
-- Test: Member can see the account
select makerkit.authenticate_as('vistest2');
select isnt_empty(
$$ SELECT * FROM public.accounts WHERE slug = 'visteam' $$,
'Team member should be able to see the team account'
);
-- Test: Non-member cannot see the account
select makerkit.authenticate_as('vistest3');
select is_empty(
$$ SELECT * FROM public.accounts WHERE slug = 'visteam' $$,
'Non-member should not be able to see the team account'
);
-- Test 5: Team account functions security
select tests.create_supabase_user('functest1', 'functest1@test.com');
select tests.create_supabase_user('functest2', 'functest2@test.com');
-- Create team account
select makerkit.authenticate_as('functest1');
select public.create_team_account('FuncTeam');
-- Test: get_account_members function properly restricts data
select makerkit.authenticate_as('functest2');
select is_empty(
$$ SELECT * FROM public.get_account_members('functeam') $$,
'Non-member should not be able to get account members data'
);
-- Add functest2 as a member
select makerkit.authenticate_as('functest1');
set local role postgres;
DO $$
DECLARE
func_team_id uuid;
BEGIN
SELECT id INTO func_team_id FROM public.accounts WHERE slug = 'functeam';
-- Add member with explicit account_id
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
VALUES (func_team_id, tests.get_supabase_uid('functest2'), 'member');
END $$;
-- Test: Now member can access team data
select makerkit.authenticate_as('functest2');
select isnt_empty(
$$ SELECT * FROM public.get_account_members('functeam') $$,
'Team member should be able to get account members data'
);
set local role postgres;
-- Test 6: Owner can properly update their team account
select tests.create_supabase_user('ownerupdate1', 'ownerupdate1@test.com');
select tests.create_supabase_user('ownerupdate2', 'ownerupdate2@test.com');
-- Create team account
select makerkit.authenticate_as('ownerupdate1');
select public.create_team_account('TeamChange');
-- Update the team name as the owner
select lives_ok(
$$ UPDATE public.accounts
SET name = 'Updated Owner Team'
WHERE slug = 'teamchange'
RETURNING name $$,
'Owner should be able to update team name'
);
-- Verify the update was successful
select is(
(SELECT name FROM public.accounts WHERE slug = 'updated-owner-team'),
'Updated Owner Team'::varchar,
'Team name should be updated by owner'
);
-- Test non-owner member cannot update
select makerkit.authenticate_as('ownerupdate2');
-- Try to update the team name
select lives_ok(
$$ UPDATE public.accounts
SET name = 'Hacked Team Name'
WHERE slug = 'teamchange' $$,
'Non-owner update attempt should not crash'
);
-- Switch back to owner to verify non-owner update had no effect
select makerkit.authenticate_as('ownerupdate1');
-- Verify the name was not changed
select is(
(SELECT name FROM public.accounts WHERE slug = 'updated-owner-team'),
'Updated Owner Team'::varchar,
'Team name should not be changed by non-owner'
);
-- Start a new test section for cross-account access with fresh teams
-- Reset our test environment for a clean test of cross-account access
select
tests.create_supabase_user('crosstest1', 'crosstest1@test.com');
select
tests.create_supabase_user('crosstest2', 'crosstest2@test.com');
-- Create first team account with crosstest1 as owner
select
makerkit.authenticate_as('crosstest1');
select
public.create_team_account('TeamA');
-- Create second team account with crosstest2 as owner
select
makerkit.authenticate_as('crosstest2');
select
public.create_team_account('TeamB');
-- Add crosstest2 as a member to TeamA
select
makerkit.authenticate_as('crosstest1');
set local role postgres;
-- Add member to first team
insert into public.accounts_memberships (account_id, user_id, account_role)
values (
(select id from makerkit.get_account_by_slug('teama')),
tests.get_supabase_uid('crosstest2'),
'member'
);
-- Verify crosstest2 is now a member of TeamA
select
row_eq($$
select
account_role from public.accounts_memberships
where
account_id = (select id from makerkit.get_account_by_slug('teama'))
and user_id = tests.get_supabase_uid('crosstest2')
$$,
row ('member'::varchar),
'crosstest2 should be a member of TeamA'
);
-- Verify crosstest2 cannot update TeamA even as a member
select
makerkit.authenticate_as('crosstest2');
-- Try to update the team name
select
lives_ok($$
update public.accounts
set name = 'Updated TeamA Name'
where id = (select id from makerkit.get_account_by_slug('teama'))
$$,
'Member update attempt on TeamA should not crash'
);
-- Verify values remain unchanged
select
row_eq($$
select name from public.accounts
where id = (select id from makerkit.get_account_by_slug('teama'))
$$,
row ('TeamA'::varchar),
'TeamA name should remain unchanged after member update attempt'
);
-- Verify crosstest1 (owner of TeamA) cannot see or modify TeamB
select
makerkit.authenticate_as('crosstest1');
select
is_empty($$
select * from public.accounts
where id = (select id from makerkit.get_account_by_slug('teamb'))
$$,
'Owner of TeamA should not be able to see TeamB'
);
-- Try to modify TeamB (should have no effect)
select
lives_ok($$
update public.accounts
set name = 'Hacked TeamB Name'
where id = (select id from makerkit.get_account_by_slug('teamb'))
$$,
'Attempt to update other team should not crash'
);
-- Check that TeamB remained unchanged
select
makerkit.authenticate_as('crosstest2');
select
row_eq($$
select name from public.accounts
where id = (select id from makerkit.get_account_by_slug('teamb'))
$$,
row ('TeamB'::varchar),
'TeamB name should remain unchanged after attempted update by non-member'
);
select
*
from
finish();
rollback;

View File

@@ -1,113 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
INSERT INTO public.billing_customers(account_id, provider, customer_id)
VALUES (makerkit.get_account_id_by_slug('makerkit'), 'stripe', 'cus_test');
-- Call the upsert_order function
SELECT public.upsert_order(makerkit.get_account_id_by_slug('makerkit'), 'cus_test', 'order_test', 'pending', 'stripe', 100, 'usd', '[
{"id":"order_item_1", "product_id": "prod_test", "variant_id": "var_test", "price_amount": 100, "quantity": 1},
{"id":"order_item_2", "product_id": "prod_test", "variant_id": "var_test_2", "price_amount": 100, "quantity": 1},
{"id":"order_item_3", "product_id": "prod_test", "variant_id": "var_test_3", "price_amount": 100, "quantity": 1},
{"id":"order_item_4", "product_id": "prod_test", "variant_id": "var_test_4", "price_amount": 100, "quantity": 1}
]');
-- Verify that the order was created correctly
SELECT is(
(SELECT status FROM public.orders WHERE id = 'order_test'),
'pending',
'The order status should be pending'
);
-- Verify that the subscription items were created correctly
SELECT row_eq(
$$ select count(*) from order_items where order_id = 'order_test' $$,
row(4::bigint),
'The order items should be created'
);
-- Call the upsert_order function again to update the order
SELECT public.upsert_order(makerkit.get_account_id_by_slug('makerkit'), 'cus_test', 'order_test', 'succeeded', 'stripe', 100, 'usd', '[
{"id":"order_item_1", "product_id": "prod_test", "variant_id": "var_test", "price_amount": 100, "quantity": 1},
{"id":"order_item_2", "product_id": "prod_test_2", "variant_id": "var_test_4", "price_amount": 200, "quantity": 10}
]');
-- Verify that the subscription items were created correctly
SELECT row_eq(
$$ select count(*) from order_items where order_id = 'order_test' $$,
row(2::bigint),
'The order items should be updated'
);
-- Verify that the order was updated correctly
SELECT is(
(SELECT status FROM public.orders WHERE id = 'order_test'),
'succeeded',
'The order status should be succeeded'
);
SELECT row_eq(
$$ select quantity from order_items where variant_id = 'var_test_4' $$,
row(10::int),
'The subscription items quantity should be updated'
);
SELECT row_eq(
$$ select variant_id from order_items where id = 'order_item_2' $$,
row('var_test_4'::text),
'The subscription items variant_id should be updated'
);
SELECT row_eq(
$$ select product_id from order_items where id = 'order_item_2' $$,
row('prod_test_2'::text),
'The subscription items prod_test_2 should be updated'
);
SELECT row_eq(
$$ select price_amount from order_items where variant_id = 'var_test_4' $$,
row(200::numeric),
'The subscription items price_amount should be updated'
);
select makerkit.authenticate_as('member');
-- account can read their own subscription
SELECT isnt_empty(
$$ select 1 from orders where id = 'order_test' $$,
'The account can read their own order'
);
SELECT isnt_empty(
$$ select * from order_items where order_id = 'order_test' $$,
'The account can read their own order'
);
-- members without permissions
-- foreigners
select tests.create_supabase_user('foreigner');
select makerkit.authenticate_as('foreigner');
-- account cannot read other's subscription
SELECT is_empty(
$$ select 1 from orders where id = 'order_test' $$,
'The account cannot read the other account orders'
);
SELECT is_empty(
$$ select 1 from order_items where order_id = 'order_test' $$,
'The account cannot read the other account order items'
);
-- Finish the tests and clean up
SELECT * FROM finish();
ROLLBACK;

View File

@@ -1,196 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
-- Create a test account and billing customer
INSERT INTO public.billing_customers(account_id, provider, customer_id)
VALUES (makerkit.get_account_id_by_slug('makerkit'), 'stripe', 'cus_test');
-- Call the upsert_subscription function
SELECT public.upsert_subscription(makerkit.get_account_id_by_slug('makerkit'), 'cus_test', 'sub_test', true, 'active', 'stripe', false, 'usd', now(), now() + interval '1 month', '[
{
"id": "sub_123",
"product_id": "prod_test",
"variant_id": "var_test",
"type": "flat",
"price_amount": 1000,
"quantity": 1,
"interval": "month",
"interval_count": 1
},
{
"id": "sub_456",
"product_id": "prod_test_2",
"variant_id": "var_test_2",
"type": "flat",
"price_amount": 2000,
"quantity": 2,
"interval": "month",
"interval_count": 1
},
{
"id": "sub_789",
"product_id": "prod_test_3",
"variant_id": "var_test_3",
"type": "flat",
"price_amount": 2000,
"quantity": 2,
"interval": "month",
"interval_count": 1
}
]');
-- Verify that the subscription items were created correctly
SELECT row_eq(
$$ select count(*) from subscription_items where subscription_id = 'sub_test' $$,
row(3::bigint),
'The subscription items should be created'
);
-- Verify that the subscription was created correctly
SELECT is(
(SELECT active FROM public.subscriptions WHERE id = 'sub_test'),
true,
'The subscription should be active'
);
SELECT is(
(SELECT status FROM public.subscriptions WHERE id = 'sub_test'),
'active',
'The subscription status should be active'
);
-- Call the upsert_subscription function again to update the subscription
SELECT public.upsert_subscription(makerkit.get_account_id_by_slug('makerkit'), 'cus_test', 'sub_test', false, 'past_due', 'stripe', true, 'usd', now(), now() + interval '1 month', '[
{
"id": "sub_123",
"product_id": "prod_test",
"variant_id": "var_test",
"type": "flat",
"price_amount": 2000,
"quantity": 1,
"interval": "month",
"interval_count": 1
},
{
"id": "sub_456",
"product_id": "prod_test_3",
"variant_id": "var_test_2",
"type": "flat",
"price_amount": 2000,
"quantity": 2,
"interval": "year",
"interval_count": 12
}
]');
SELECT row_eq(
$$ select count(*) from subscription_items where subscription_id = 'sub_test' $$,
row(2::bigint),
'The subscription items should be updated'
);
-- Verify that the subscription items were updated correctly
SELECT row_eq(
$$ select price_amount from subscription_items where variant_id = 'var_test' $$,
row('2000'::numeric),
'The subscription items price_amount should be updated'
);
-- Verify that the subscription items were updated correctly
SELECT row_eq(
$$ select interval from subscription_items where variant_id = 'var_test_2' $$,
row('year'::varchar),
'The subscription items interval should be updated'
);
-- Verify that the subscription items were updated correctly
SELECT row_eq(
$$ select product_id from subscription_items where id = 'sub_456' $$,
row('prod_test_3'::varchar),
'The subscription items product_id should be updated'
);
-- Verify that the subscription was updated correctly
SELECT is(
(SELECT active FROM public.subscriptions WHERE id = 'sub_test'),
false,
'The subscription should be inactive'
);
SELECT is(
(SELECT status FROM public.subscriptions WHERE id = 'sub_test'),
'past_due',
'The subscription status should be past_due'
);
select makerkit.authenticate_as('member');
SELECT row_eq(
$$ select count(*) from subscription_items where subscription_id = 'sub_test' $$,
row(2::bigint),
'The member can also read the subscription items'
);
set role service_role;
-- Call the upsert_subscription function again to update the subscription
SELECT public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_test', 'sub_test', true, 'active', 'stripe', false, 'usd', now(), now() + interval '1 month', '[]');
-- Verify that the subscription was updated correctly
SELECT is(
(SELECT active FROM public.subscriptions WHERE id = 'sub_test'),
true,
'The subscription should be active'
);
select makerkit.authenticate_as('member');
-- account can read their own subscription
select isnt_empty(
$$ select 1 from subscriptions where id = 'sub_test' $$,
'The account can read their own subscription'
);
select is_empty(
$$ select * from subscription_items where subscription_id = 'sub_test' $$,
'The subscription items are now empty'
);
select is(
(public.has_active_subscription(makerkit.get_account_id_by_slug('makerkit'))),
true,
'The function public.has_active_subscription should return true when the account has a subscription'
);
-- foreigners
select tests.create_supabase_user('foreigner');
select makerkit.authenticate_as('foreigner');
-- account cannot read other's subscription
select is_empty(
$$ select 1 from subscriptions where id = 'sub_test' $$,
'The account cannot read the other account subscriptions'
);
select is_empty(
$$ select 1 from subscription_items where subscription_id = 'sub_test' $$,
'The account cannot read the other account subscription items'
);
select is(
(public.has_active_subscription(makerkit.get_account_id_by_slug('makerkit'))),
false,
'The function public.has_active_subscription should return false when a foreigner is querying the account subscription'
);
-- Finish the tests and clean up
select * from finish();
rollback;

View File

@@ -1,73 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
-- another user not in the team
select tests.create_supabase_user('test', 'test@supabase.com');
-- auth as a primary owner
select makerkit.authenticate_as('primary_owner');
-- only the service role can transfer ownership
select throws_ok(
$$ select public.transfer_team_account_ownership(
makerkit.get_account_id_by_slug('makerkit'),
tests.get_supabase_uid('custom')
) $$,
'permission denied for function transfer_team_account_ownership'
);
set local role service_role;
-- the new owner must be a member of the account so this should fail
select throws_ok(
$$ select public.transfer_team_account_ownership(
makerkit.get_account_id_by_slug('makerkit'),
tests.get_supabase_uid('test')
) $$,
'The new owner must be a member of the account'
);
-- this should work because the user is a member of the account
select lives_ok(
$$ select public.transfer_team_account_ownership(
makerkit.get_account_id_by_slug('makerkit'),
tests.get_supabase_uid('owner')
) $$
);
-- check the account owner has been updated
select row_eq(
$$ select primary_owner_user_id from public.accounts where id = makerkit.get_account_id_by_slug('makerkit') $$,
row(tests.get_supabase_uid('owner')),
'The account owner should be updated'
);
-- when transferring ownership to an account with a lower role
-- the account will also be updated to the new role
select lives_ok(
$$ select public.transfer_team_account_ownership(
makerkit.get_account_id_by_slug('makerkit'),
tests.get_supabase_uid('member')
) $$
);
-- check the account owner has been updated
select row_eq(
$$ select account_role from public.accounts_memberships
where account_id = makerkit.get_account_id_by_slug('makerkit')
and user_id = tests.get_supabase_uid('member');
$$,
row('owner'::varchar),
'The account owner should be updated'
);
select * from finish();
rollback;

View File

@@ -1,27 +0,0 @@
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
-- another user not in the team
select tests.create_supabase_user('test', 'test@supabase.com');
select makerkit.authenticate_as('member');
-- run an update query
update public.accounts_memberships set account_role = 'owner' where user_id = auth.uid() and account_id = makerkit.get_account_id_by_slug('makerkit');
select row_eq(
$$ select account_role from public.accounts_memberships where user_id = auth.uid() and account_id = makerkit.get_account_id_by_slug('makerkit'); $$,
row('member'::varchar),
'Updates fail silently to any field of the accounts_membership table'
);
select * from finish();
rollback;