B2B-88: remove supabase copy
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
4
supabase copy/.gitignore
vendored
4
supabase copy/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
# Supabase
|
|
||||||
.branches
|
|
||||||
.temp
|
|
||||||
.env
|
|
||||||
@@ -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
@@ -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');
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
create policy delete_team_account
|
|
||||||
on public.accounts
|
|
||||||
for delete
|
|
||||||
to authenticated
|
|
||||||
using (
|
|
||||||
auth.uid() = primary_owner_user_id
|
|
||||||
);
|
|
||||||
@@ -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';
|
|
||||||
@@ -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());
|
|
||||||
@@ -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 '';
|
|
||||||
@@ -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;
|
|
||||||
@@ -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));
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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)
|
|
||||||
);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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)
|
|
||||||
);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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 ();
|
|
||||||
@@ -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';
|
|
||||||
@@ -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());
|
|
||||||
@@ -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());
|
|
||||||
@@ -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;
|
|
||||||
@@ -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'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@@ -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');
|
|
||||||
@@ -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
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
Reference in New Issue
Block a user