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.