From fce4c0e0a533a536e02b6f2daf9e16059f17fa0b Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 22 Sep 2024 12:06:39 +0100 Subject: [PATCH] feat(#29): Add example product tool requiring purchase to access --- .env.example | 3 +- docs/database-setup.md | 7 ++++ src/lib/components/NavLinks.svelte | 2 +- src/lib/types/supabase.d.ts | 10 +++++ src/lib/utils/supabase/admin.ts | 37 ++++++++++++++++++- src/routes/checkout/+page.server.ts | 11 ++++++ src/routes/login/+page.svelte | 9 +++++ .../tools/exampleproduct/+page.server.ts | 19 ++++++++++ src/routes/tools/exampleproduct/+page.svelte | 2 + src/routes/unauthorised/+page.svelte | 2 + .../20240704190621_initial_migration.sql | 2 + 11 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/routes/checkout/+page.server.ts create mode 100644 src/routes/login/+page.svelte create mode 100644 src/routes/tools/exampleproduct/+page.server.ts create mode 100644 src/routes/tools/exampleproduct/+page.svelte create mode 100644 src/routes/unauthorised/+page.svelte diff --git a/.env.example b/.env.example index 507a8e7..6d0104e 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,5 @@ PUBLIC_SUPABASE_URL="" PUBLIC_SUPABASE_ANON_KEY="" SUPABASE_SERVICE_ROLE_KEY="" STRIPE_SECRET_KEY="" -STRIPE_ENDPOINT_SECRET="" \ No newline at end of file +STRIPE_ENDPOINT_SECRET="" +VITE_PRODUCT_ID_EXAMPLEPRODUCT="" \ No newline at end of file diff --git a/docs/database-setup.md b/docs/database-setup.md index 5814ff1..b40e51c 100644 --- a/docs/database-setup.md +++ b/docs/database-setup.md @@ -6,6 +6,13 @@ Databases are stored in Supabase. The auth table for signing up and logging in u To add the tables for recording their active subscriptions and bought products, as well as products and prices, you can use the SQL scripts from the `supabase/migrations` folder in the Supabase SQL Editor. +## Common commands for local development + +- `npx supabase start` - Start the local Supabase instance +- `npx supabase stop` - Stop the local Supabase instance +- `npx supabase db reset` - Reset the local database +- `npx supabase gen types typescript --local > src/lib/types/supabase.d.ts` - Generate types for the local database + ## Generating Types To get the types for the tables, you can follow these instructions taken from [here](https://supabase.com/docs/guides/api/rest/generating-types#generating-types-using-supabase-cli): diff --git a/src/lib/components/NavLinks.svelte b/src/lib/components/NavLinks.svelte index fb054f5..fd73fff 100644 --- a/src/lib/components/NavLinks.svelte +++ b/src/lib/components/NavLinks.svelte @@ -20,4 +20,4 @@
  • Submenu 2
  • -
  • Item 3
  • +
  • Example Product
  • diff --git a/src/lib/types/supabase.d.ts b/src/lib/types/supabase.d.ts index 42f3532..a79afeb 100644 --- a/src/lib/types/supabase.d.ts +++ b/src/lib/types/supabase.d.ts @@ -201,6 +201,7 @@ export type Database = { id: string metadata: Json | null price_id: string | null + product_id: string quantity: number | null status: Database["public"]["Enums"]["subscription_status"] | null trial_end: string | null @@ -218,6 +219,7 @@ export type Database = { id: string metadata?: Json | null price_id?: string | null + product_id: string quantity?: number | null status?: Database["public"]["Enums"]["subscription_status"] | null trial_end?: string | null @@ -235,6 +237,7 @@ export type Database = { id?: string metadata?: Json | null price_id?: string | null + product_id?: string quantity?: number | null status?: Database["public"]["Enums"]["subscription_status"] | null trial_end?: string | null @@ -249,6 +252,13 @@ export type Database = { referencedRelation: "prices" referencedColumns: ["id"] }, + { + foreignKeyName: "subscriptions_product_id_fkey" + columns: ["product_id"] + isOneToOne: false + referencedRelation: "products" + referencedColumns: ["id"] + }, { foreignKeyName: "subscriptions_user_id_fkey" columns: ["user_id"] diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts index 1e0bac9..5dafd4f 100644 --- a/src/lib/utils/supabase/admin.ts +++ b/src/lib/utils/supabase/admin.ts @@ -211,6 +211,7 @@ const manageSubscriptionStatusChange = async ( user_id: uuid, metadata: subscription.metadata, status: subscription.status, + product_id: subscription.items.data[0].price.product as string, price_id: subscription.items.data[0].price.id, quantity: 1, //subscription.quantity, cancel_at_period_end: subscription.cancel_at_period_end, @@ -264,6 +265,39 @@ const recordProductPurchase = async (userId: string, productId: string, priceId: console.log(`Product purchased recorded for user [${userId}] and product [${productId}]`); }; +const hasProductAccess = async (userId: string, productId: string) => { + // Check if user has purchased the product + const { data: purchases, error: purchaseError } = await supabaseAdmin + .from('purchases') + .select('*') + .eq('user_id', userId) + .eq('product_id', productId); + + if (purchaseError) throw new Error(purchaseError.message); + + if (purchases && purchases.length > 0) { + // User has purchased the product + return true; + } + + // Check if user has an active subscription for the product + const { data: subscriptions, error: subError } = await supabaseAdmin + .from('subscriptions') + .select('*') + .eq('user_id', userId) + .eq('product_id', productId) + .in('status', ['active', 'trialing']); + + if (subError) throw new Error(subError.message); + + if (subscriptions && subscriptions.length > 0) { + // User has an active subscription + return true; + } + + return false; +}; + export { upsertProductRecord, upsertPriceRecord, @@ -271,5 +305,6 @@ export { deletePriceRecord, createOrRetrieveCustomer, manageSubscriptionStatusChange, - recordProductPurchase + recordProductPurchase, + hasProductAccess }; diff --git a/src/routes/checkout/+page.server.ts b/src/routes/checkout/+page.server.ts new file mode 100644 index 0000000..8682d8c --- /dev/null +++ b/src/routes/checkout/+page.server.ts @@ -0,0 +1,11 @@ +/** @type {import('./$types').PageLoad} */ +import { redirect } from '@sveltejs/kit'; + +export const load = async ({ params, locals: { safeGetSession } }) => { + const { session } = await safeGetSession(); + if (!session) { + throw redirect(303, '/login'); // Redirect unauthenticated users to login + } + + return params; +}; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte new file mode 100644 index 0000000..10b4694 --- /dev/null +++ b/src/routes/login/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/tools/exampleproduct/+page.server.ts b/src/routes/tools/exampleproduct/+page.server.ts new file mode 100644 index 0000000..4721b3c --- /dev/null +++ b/src/routes/tools/exampleproduct/+page.server.ts @@ -0,0 +1,19 @@ +/** @type {import('./$types').PageLoad} */ +import { redirect } from '@sveltejs/kit'; +import { hasProductAccess } from '$lib/utils/supabase/admin'; +import { VITE_PRODUCT_ID_EXAMPLEPRODUCT } from '$env/static/private'; // Import the example product ID from env + +export const load = async ({ params, locals: { safeGetSession } }) => { + const { session } = await safeGetSession(); + if (!session) { + throw redirect(303, '/login'); // Redirect unauthenticated users to login + } + + // Check if the user has access to the 'EXAMPLE PRODUCT' product + const accessGranted = await hasProductAccess(session.user.id, VITE_PRODUCT_ID_EXAMPLEPRODUCT); + if (!accessGranted) { + throw redirect(303, '/unauthorised'); + } + + return params; +}; diff --git a/src/routes/tools/exampleproduct/+page.svelte b/src/routes/tools/exampleproduct/+page.svelte new file mode 100644 index 0000000..c969ac6 --- /dev/null +++ b/src/routes/tools/exampleproduct/+page.svelte @@ -0,0 +1,2 @@ +

    Example Product

    +

    This is an example of a tool you could be selling.

    diff --git a/src/routes/unauthorised/+page.svelte b/src/routes/unauthorised/+page.svelte new file mode 100644 index 0000000..29a9468 --- /dev/null +++ b/src/routes/unauthorised/+page.svelte @@ -0,0 +1,2 @@ +

    Unauthorised Access

    +

    Sorry, you don't have permission to access this tool.

    diff --git a/supabase/migrations/20240704190621_initial_migration.sql b/supabase/migrations/20240704190621_initial_migration.sql index 6da61f2..3e81bec 100644 --- a/supabase/migrations/20240704190621_initial_migration.sql +++ b/supabase/migrations/20240704190621_initial_migration.sql @@ -110,6 +110,8 @@ create table subscriptions ( status subscription_status, -- Set of key-value pairs, used to store additional information about the object in a structured format. metadata jsonb, + -- ID of the product this subscription is for. + product_id text references products(id) not null, -- ID of the price that created this subscription. price_id text references prices, -- Quantity multiplied by the unit amount of the price creates the amount of the subscription. Can be used to charge multiple seats.