14 Commits

Author SHA1 Message Date
OllyNicholass 2dfb7329a8 chore(#15): update db_schema types 2024-06-19 01:24:54 +01:00
OllyNicholass 1f35285525 fix(#15): remove email field from profiles 2024-06-19 01:23:59 +01:00
OllyNicholass 65d33f1f58 docs(#15): docs to update db_schema 2024-06-19 01:21:36 +01:00
OllyNicholass c374d686fb chore(#15): update db_schema types 2024-06-19 01:21:03 +01:00
OllyNicholass baa515ca55 chore(#15): add migration to add profiles trigger 2024-06-19 00:44:43 +01:00
OllyNicholass 2dc67875d5 Merge branch '15-add-supabase-local-development-to-allow-for-migrations' of https://github.com/jcreek/SvelteKitSaasBoilerplate into 15-add-supabase-local-development-to-allow-for-migrations 2024-06-18 21:57:05 +01:00
OllyNicholass a22883991d chore(#15): add migration to create profiles table 2024-06-18 21:56:43 +01:00
OllyNicholass 6b6ed219fb docs(#15): add local supabase help to readme 2024-06-18 21:56:43 +01:00
OllyNicholass d9b76e4609 chore(#15): init local supabase development 2024-06-18 21:56:43 +01:00
OllyNicholass 1a252be011 deps(#15): add supabase cli package 2024-06-18 21:56:42 +01:00
OllyNicholass 8217328751 chore(#15): add migration to create profiles table 2024-06-04 19:22:27 +01:00
OllyNicholass 5bfc4dedd2 docs(#15): add local supabase help to readme 2024-06-04 19:20:56 +01:00
OllyNicholass bf4def1b1d chore(#15): init local supabase development 2024-06-04 19:20:24 +01:00
OllyNicholass 794ece2a14 deps(#15): add supabase cli package 2024-06-04 19:07:19 +01:00
34 changed files with 757 additions and 2000 deletions
+1
View File
@@ -8,4 +8,5 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/static/output.css
.netlify
+72 -6
View File
@@ -17,6 +17,78 @@ npm run dev -- --open
An example `.env` file is provided in the repository. You will need to copy `.env.example` to `.env` and fill in the values with your own credentials.
### Local DB Development
Ensure you have [Docker](https://docs.docker.com/get-docker/) installed on your development machine.
You can start and stop the local DB environment with:
```bash
npx supabase start
npx supabase stop
```
#### Generate schema types (Typescript)
`npx supabase gen types typescript --local > ./src/lib/db_schema.ts`
The overarching `Database` type is used for a generic `SupabaseClient`. This is set globally in `src/app.d.ts`
```ts
import { SupabaseClient } from '@supabase/supabase-js'
import { Database } from '$lib/db_schema'
declare global {
namespace App {
interface Locals {
supabase: SupabaseClient<Database>
...
}
}
}
```
#### Migrations
There are two options to create a migration file:
1. [Writing the SQL manually](https://supabase.com/docs/guides/cli/local-development#database-migrations)
- Create new blank migration file
```bash
npx supabase migration new <migration_name>
```
- Add the SQL you want to deploy as part of the migration e.g.
```sql
create table
employees (
id bigint primary key generated always as identity,
name text,
email text,
created_at timestamptz default now()
);
```
- Reset the DB and apply the latest migrations
```bash
supabase db reset
```
2. [Generate SQL based on differences to the schema](https://supabase.com/docs/guides/cli/local-development#diffing-changes)
- With the local DB running, enter the studio and make your changes to the schema
- Generate the migration file containing the SQL with the differences in the schema
```bash
npx supabase db diff -f <migration_name>
```
## Building
To create a production version:
@@ -41,10 +113,4 @@ In its current configuration, the application can be easily linked to Netlify vi
## To Do
- [Update types automatically](https://supabase.com/docs/guides/api/rest/generating-types#update-types-automatically-with-github-actions)
Please see the Issues tab for `enhancement` tagged issues.
## [Database setup](/docs/database-setup.md)
## [Stripe webhook event listening](/docs/stripe-setup.md)
-239
View File
@@ -1,239 +0,0 @@
# Database Setup
Databases are stored in Supabase. The auth table for signing up and logging in users is created automatically when you set up Supabase Auth.
## Initial Setup
To add the tables for recording their active subscriptions and bought products, as well as products and prices, you can use the following SQL script in the Supabase SQL Editor.
```sql
/**
* USERS
* Note: This table contains user data. Users should only be able to view and update their own data.
*/
create table users (
-- UUID from auth.users
id uuid references auth.users not null primary key,
full_name text,
avatar_url text,
-- The customer's billing address, stored in JSON format.
billing_address jsonb,
-- Stores your customer's payment instruments.
payment_method jsonb
);
alter table users
enable row level security;
create policy "Can view own user data." on users
for select using ((select auth.uid()) = id);
create policy "Can update own user data." on users
for update using ((select auth.uid()) = id);
/**
* This trigger automatically creates a user entry when a new user signs up via Supabase Auth.
*/
create function public.handle_new_user()
returns trigger as
$$
begin
insert into public.users (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$
language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row
execute procedure public.handle_new_user();
/**
* CUSTOMERS
* Note: this is a private table that contains a mapping of user IDs to Stripe customer IDs.
*/
create table customers (
-- UUID from auth.users
id uuid references auth.users not null primary key,
-- The user's customer ID in Stripe. User must not be able to update this.
stripe_customer_id text
);
alter table customers enable row level security;
-- No policies as this is a private table that the user must not have access to.
/**
* PRODUCTS
* Note: products are created and managed in Stripe and synced to our DB via Stripe webhooks.
*/
create table products (
-- Product ID from Stripe, e.g. prod_1234.
id text primary key,
-- Whether the product is currently available for purchase.
active boolean,
-- The product's name, meant to be displayable to the customer. Whenever this product is sold via a subscription, name will show up on associated invoice line item descriptions.
name text,
-- The product's description, meant to be displayable to the customer. Use this field to optionally store a long form explanation of the product being sold for your own rendering purposes.
description text,
-- A URL of the product image in Stripe, meant to be displayable to the customer.
image text,
-- Set of key-value pairs, used to store additional information about the object in a structured format.
metadata jsonb
);
alter table products
enable row level security;
create policy "Allow public read-only access." on products
for select using (true);
/**
* PRICES
* Note: prices are created and managed in Stripe and synced to our DB via Stripe webhooks.
*/
create type pricing_type as enum ('one_time', 'recurring');
create type pricing_plan_interval as enum ('day', 'week', 'month', 'year');
create table prices (
-- Price ID from Stripe, e.g. price_1234.
id text primary key,
-- The ID of the prduct that this price belongs to.
product_id text references products,
-- Whether the price can be used for new purchases.
active boolean,
-- A brief description of the price.
description text,
-- The unit amount as a positive integer in the smallest currency unit (e.g., 100 cents for US$1.00 or 100 for ¥100, a zero-decimal currency).
unit_amount bigint,
-- Three-letter ISO currency code, in lowercase.
currency text check (char_length(currency) = 3),
-- One of `one_time` or `recurring` depending on whether the price is for a one-time purchase or a recurring (subscription) purchase.
type pricing_type,
-- The frequency at which a subscription is billed. One of `day`, `week`, `month` or `year`.
interval pricing_plan_interval,
-- The number of intervals (specified in the `interval` attribute) between subscription billings. For example, `interval=month` and `interval_count=3` bills every 3 months.
interval_count integer,
-- Default number of trial days when subscribing a customer to this price using [`trial_from_plan=true`](https://stripe.com/docs/api#create_subscription-trial_from_plan).
trial_period_days integer,
-- Set of key-value pairs, used to store additional information about the object in a structured format.
metadata jsonb
);
alter table prices
enable row level security;
create policy "Allow public read-only access." on prices
for select using (true);
/**
* SUBSCRIPTIONS
* Note: subscriptions are created and managed in Stripe and synced to our DB via Stripe webhooks.
*/
create type subscription_status as enum ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid');
create table subscriptions (
-- Subscription ID from Stripe, e.g. sub_1234.
id text primary key,
user_id uuid references auth.users not null,
-- The status of the subscription object, one of subscription_status type above.
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 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.
quantity integer,
-- If true the subscription has been canceled by the user and will be deleted at the end of the billing period.
cancel_at_period_end boolean,
-- Time at which the subscription was created.
created timestamp with time zone default timezone('utc'::text, now()) not null,
-- Start of the current period that the subscription has been invoiced for.
current_period_start timestamp with time zone default timezone('utc'::text, now()) not null,
-- End of the current period that the subscription has been invoiced for. At the end of this period, a new invoice will be created.
current_period_end timestamp with time zone default timezone('utc'::text, now()) not null,
-- If the subscription has ended, the timestamp of the date the subscription ended.
ended_at timestamp with time zone default timezone('utc'::text, now()),
-- A date in the future at which the subscription will automatically get canceled.
cancel_at timestamp with time zone default timezone('utc'::text, now()),
-- If the subscription has been canceled, the date of that cancellation. If the subscription was canceled with `cancel_at_period_end`, `canceled_at` will still reflect the date of the initial cancellation request, not the end of the subscription period when the subscription is automatically moved to a canceled state.
canceled_at timestamp with time zone default timezone('utc'::text, now()),
-- If the subscription has a trial, the beginning of that trial.
trial_start timestamp with time zone default timezone('utc'::text, now()),
-- If the subscription has a trial, the end of that trial.
trial_end timestamp with time zone default timezone('utc'::text, now())
);
alter table subscriptions
enable row level security;
create policy "Can only view own subs data." on subscriptions
for select using ((select auth.uid()) = user_id);
/**
* REALTIME SUBSCRIPTIONS
* Only allow realtime listening on public tables.
*/
drop publication if exists supabase_realtime;
create publication supabase_realtime
for table products, prices;
```
## 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):
`npm i supabase@">=1.8.1" --save-dev`
`npx supabase login`
`npx supabase init`
Replace `$PROJECT_REF` with your project reference:
`npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts`
Replace `types/supabase.d.ts` with whatever file you want your types in:
`npx supabase gen types typescript --local > types/supabase.d.ts`
## Local DB Development
Ensure you have [Docker](https://docs.docker.com/get-docker/) installed and running.
You can start and stop the local DB environment with:
```bash
npx supabase start
npx supabase stop
```
## Migrations
There are two options to create a migration file:
1. [Writing the SQL manually](https://supabase.com/docs/guides/cli/local-development#database-migrations)
- Create new blank migration file
`npx supabase migration new <migration_name>`
- Add the SQL you want to deploy as part of the migration e.g.
```sql
create table
employees (
id bigint primary key generated always as identity,
name text,
email text,
created_at timestamptz default now()
);
```
- Reset the DB and apply the latest migrations
`npx supabase db reset`
2. [Generate SQL based on differences to the schema](https://supabase.com/docs/guides/cli/local-development#diffing-changes)
- With the local DB running, enter the studio and make your changes to the schema
- Generate the migration file containing the SQL with the differences in the schema
`npx supabase db diff -f <migration_name>`
## Applying migrations
Reset database in cloud
> Prerequisite login using `npx supabase login`
`npx supabase db reset --linked`
-35
View File
@@ -1,35 +0,0 @@
# Stripe webhook event listening
> First ensure you've installed the [Stripe CLI](https://docs.stripe.com/stripe-cli)
Log in using the stripe CLI:
```bash
stripe login
```
Obtain your Webhook signing secret:
```bash
stripe listen
```
Add the secret key to to your .env(.local):
`PUBLIC_STRIPE_ENDPOINT_SECRET`
### Testing the webhook locally
You can use the [Stripe CLI to forward events to your local server](https://docs.stripe.com/webhooks):
```bash
stripe listen --forward-to localhost:5173/api/webhook/stripe
```
To disable HTTPS certificate verification, use the --skip-verify optional flag.
```bash
stripe listen --forward-to localhost:5173/api/webhook/stripe --skip-verify
```
TODO - Add instructions for what else to do to get the webhook working locally.
+135 -305
View File
@@ -8,8 +8,8 @@
"name": "SvelteKitSaasBoilerplate",
"version": "0.0.1",
"dependencies": {
"@supabase/ssr": "^0.4.0",
"@supabase/supabase-js": "^2.44.2",
"@supabase/ssr": "^0.1.0",
"@supabase/supabase-js": "^2.42.0",
"stripe": "^15.4.0"
},
"devDependencies": {
@@ -19,8 +19,6 @@
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.0.6",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.13",
"@types/cookie": "^0.6.0",
"@types/eslint": "^8.56.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
@@ -32,11 +30,10 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.39",
"postcss-load-config": "^5.1.0",
"postcss": "^8.4.38",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"supabase": "^1.178.2",
"supabase": "^1.167.4",
"svelte": "^4.2.8",
"svelte-check": "^3.6.2",
"tailwindcss": "^3.4.3",
@@ -2587,7 +2584,6 @@
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
@@ -2970,6 +2966,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -3028,19 +3025,17 @@
]
},
"node_modules/@supabase/auth-js": {
"version": "2.64.2",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.2.tgz",
"integrity": "sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw==",
"license": "MIT",
"version": "2.64.1",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.1.tgz",
"integrity": "sha512-tA2PXLoWEzhD0N1Vysree+HftfeWBbFV0E+taND5rj/pZTjkwKq/9GlrnXkbs5pnw+tsnABDRo2WLZmymihGdA==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.1.tgz",
"integrity": "sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==",
"license": "MIT",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz",
"integrity": "sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
@@ -3049,7 +3044,6 @@
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -3058,19 +3052,17 @@
}
},
"node_modules/@supabase/postgrest-js": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.7.tgz",
"integrity": "sha512-TJztay5lcnnKuXjIO/X/aaajOsP8qNeW0k3MqIFoOtRolj5MEAIy8rixNakRk3o23eVCdsuP3iMLYPvOOruH6Q==",
"license": "MIT",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz",
"integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.10.2",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.2.tgz",
"integrity": "sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==",
"license": "MIT",
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.4.tgz",
"integrity": "sha512-wdq+2hZpgw0r2ldRs87d3U08Y8BrsO1bZxPNqbImpYshAEkusDz4vufR8KaqujKxqewmXS6YnUhuRVdvSEIKCA==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14",
"@types/phoenix": "^1.5.4",
@@ -3079,41 +3071,36 @@
}
},
"node_modules/@supabase/ssr": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.4.0.tgz",
"integrity": "sha512-6WS3NUvHDhCPAFN2kJ79AQDO8+M9fJ7y2fYpxgZqIuJEpnnGsHDNnB5Xnv8CiaJIuRU+0pKboy62RVZBMfZ0Lg==",
"license": "MIT",
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.1.0.tgz",
"integrity": "sha512-bIVrkqjAK5G3KjkIMKYKtAOlCgRRplEWjrlyRyXSOYtgDieiOhk2ZyNAPsEOa1By9OZVxuX5eAW1fitdnuxayw==",
"dependencies": {
"cookie": "^0.6.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.9.5"
"cookie": "^0.5.0",
"ramda": "^0.29.0"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.43.4"
"@supabase/supabase-js": "^2.33.1"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.6.0.tgz",
"integrity": "sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw==",
"license": "MIT",
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz",
"integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.44.2",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.44.2.tgz",
"integrity": "sha512-fouCwL1OxqftOwLNgdDUPlNnFuCnt30nS4kLcnTpe6NYKn1PmjxRRBFmKscgHs6FjWyU+32ZG4uBJ29+/BWiDw==",
"license": "MIT",
"version": "2.42.7",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.42.7.tgz",
"integrity": "sha512-BEIEYe5KJpzd8Z3k4CKyjNuBmgSihDdE8MJO/Fg7O5h/lQg8qOp1MMWLWPP5aVKt4TYled/W82ePNJflqc2JbQ==",
"dependencies": {
"@supabase/auth-js": "2.64.2",
"@supabase/functions-js": "2.4.1",
"@supabase/auth-js": "2.64.1",
"@supabase/functions-js": "2.3.1",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.15.7",
"@supabase/realtime-js": "2.10.2",
"@supabase/storage-js": "2.6.0"
"@supabase/postgrest-js": "1.15.2",
"@supabase/realtime-js": "2.9.4",
"@supabase/storage-js": "2.5.5"
}
},
"node_modules/@surma/rollup-plugin-off-main-thread": {
@@ -3221,6 +3208,15 @@
"vite": "^5.0.3"
}
},
"node_modules/@sveltejs/kit/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.0.tgz",
@@ -3260,46 +3256,6 @@
"vite": "^5.0.0"
}
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
"integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==",
"dev": true,
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
"integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
"dev": true,
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -3337,10 +3293,9 @@
}
},
"node_modules/@types/phoenix": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz",
"integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==",
"license": "MIT"
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz",
"integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA=="
},
"node_modules/@types/pug": {
"version": "2.0.10",
@@ -3371,7 +3326,6 @@
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -3646,7 +3600,6 @@
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
@@ -3997,7 +3950,6 @@
"resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.4.tgz",
"integrity": "sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==",
"dev": true,
"license": "ISC",
"dependencies": {
"cmd-shim": "^6.0.0",
"npm-normalize-package-bin": "^3.0.0",
@@ -4041,13 +3993,12 @@
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
@@ -4110,12 +4061,12 @@
}
},
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"engines": {
"node": ">=8.0.0"
"node": "*"
}
},
"node_modules/buffer-from": {
@@ -4265,7 +4216,6 @@
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.3.tgz",
"integrity": "sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
@@ -4387,10 +4337,9 @@
"peer": true
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"license": "MIT",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
@@ -4501,7 +4450,6 @@
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
}
@@ -5103,44 +5051,6 @@
}
}
},
"node_modules/eslint-plugin-svelte/node_modules/postcss-load-config": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
"dev": true,
"dependencies": {
"lilconfig": "^2.0.5",
"yaml": "^1.10.2"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"postcss": {
"optional": true
},
"ts-node": {
"optional": true
}
}
},
"node_modules/eslint-plugin-svelte/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -5353,7 +5263,6 @@
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
@@ -5398,11 +5307,10 @@
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -5477,7 +5385,6 @@
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
@@ -5845,11 +5752,10 @@
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
"integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
@@ -6163,7 +6069,6 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -6601,12 +6506,6 @@
"dev": true,
"peer": true
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -6614,12 +6513,6 @@
"dev": true,
"peer": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -6703,15 +6596,6 @@
"node": ">=4"
}
},
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"dev": true,
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/minimatch": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
@@ -6737,11 +6621,10 @@
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz",
"integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -6751,7 +6634,6 @@
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz",
"integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"minipass": "^7.0.4",
"rimraf": "^5.0.5"
@@ -6761,18 +6643,16 @@
}
},
"node_modules/minizlib/node_modules/glob": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz",
"integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==",
"version": "10.3.15",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz",
"integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
"jackspeak": "^2.3.6",
"minimatch": "^9.0.1",
"minipass": "^7.0.4",
"path-scurry": "^1.11.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
@@ -6784,31 +6664,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minizlib/node_modules/jackspeak": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz",
"integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/minizlib/node_modules/rimraf": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
"integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^10.3.7"
},
@@ -6938,7 +6798,6 @@
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
@@ -6948,7 +6807,6 @@
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
@@ -6991,7 +6849,6 @@
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
"integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
@@ -7107,13 +6964,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -7164,7 +7014,6 @@
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -7224,9 +7073,9 @@
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"node_modules/picomatch": {
@@ -7270,9 +7119,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"dev": true,
"funding": [
{
@@ -7290,7 +7139,7 @@
],
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"picocolors": "^1.0.0",
"source-map-js": "^1.2.0"
},
"engines": {
@@ -7334,56 +7183,34 @@
}
},
"node_modules/postcss-load-config": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.1.0.tgz",
"integrity": "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"lilconfig": "^3.1.1",
"yaml": "^2.4.2"
"lilconfig": "^2.0.5",
"yaml": "^1.10.2"
},
"engines": {
"node": ">= 18"
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"jiti": ">=1.21.0",
"postcss": ">=8.0.9",
"tsx": "^4.8.1"
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
},
"postcss": {
"optional": true
},
"tsx": {
"ts-node": {
"optional": true
}
}
},
"node_modules/postcss-load-config/node_modules/lilconfig": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/postcss-nested": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
@@ -7624,6 +7451,15 @@
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
"dev": true
},
"node_modules/ramda": {
"version": "0.29.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz",
"integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ramda"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -7672,7 +7508,6 @@
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz",
"integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
@@ -8056,7 +7891,6 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -8089,7 +7923,6 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
@@ -8330,13 +8163,13 @@
"peer": true
},
"node_modules/sorcery": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz",
"integrity": "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==",
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
"integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.14",
"buffer-crc32": "^1.0.0",
"buffer-crc32": "^0.2.5",
"minimist": "^1.2.0",
"sander": "^0.5.0"
},
@@ -8721,17 +8554,16 @@
}
},
"node_modules/supabase": {
"version": "1.178.2",
"resolved": "https://registry.npmjs.org/supabase/-/supabase-1.178.2.tgz",
"integrity": "sha512-JdjNY56cF5PbuhFdhgdYah5qXOPRsLQn0kXaY7uapTFopdIFX4tRiFXqKSig8S0SdznF/y/f6vLdq30Sek1ZZQ==",
"version": "1.167.4",
"resolved": "https://registry.npmjs.org/supabase/-/supabase-1.167.4.tgz",
"integrity": "sha512-DTaTsYQ48FdOPvTTl3H1cQnGDHvm4kQdBakeI5qbs7BFZgu2SRJDYRrCcSgH9sbZReOKrQdJCPfNpoy/Z359gA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bin-links": "^4.0.3",
"https-proxy-agent": "^7.0.2",
"node-fetch": "^3.3.2",
"tar": "7.4.0"
"tar": "7.1.0"
},
"bin": {
"supabase": "bin/supabase"
@@ -9014,16 +8846,27 @@
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/tar": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz",
"integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==",
"node_modules/tailwindcss/node_modules/yaml": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/tar": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.1.0.tgz",
"integrity": "sha512-ENhg4W6BmjYxl8GTaE7/h99f0aXiSWv4kikRZ9n2/JRxypZniE84ILZqimAhxxX7Zb8Px6pFdheW3EeHfhnXQQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minipass": "^7.1.0",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
@@ -9062,7 +8905,6 @@
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
@@ -9072,7 +8914,6 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
@@ -9088,7 +8929,6 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
@@ -9219,7 +9059,6 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -9239,8 +9078,7 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
@@ -9656,7 +9494,6 @@
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
@@ -9664,14 +9501,12 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -10246,7 +10081,6 @@
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
"signal-exit": "^4.0.1"
@@ -10256,10 +10090,9 @@
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"engines": {
"node": ">=10.0.0"
},
@@ -10283,15 +10116,12 @@
"dev": true
},
"node_modules/yaml": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
"integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
"node": ">= 6"
}
},
"node_modules/yocto-queue": {
+8 -10
View File
@@ -3,13 +3,14 @@
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"dev": "npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
"build": "npx tailwindcss -i ./static/input.css -o ./static/output.css && vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
"format": "prettier --write .",
"tailwind": "npx tailwindcss -i ./static/input.css -o ./static/output.css"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
@@ -18,8 +19,6 @@
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.0.6",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.13",
"@types/cookie": "^0.6.0",
"@types/eslint": "^8.56.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
@@ -31,11 +30,10 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.39",
"postcss-load-config": "^5.1.0",
"postcss": "^8.4.38",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"supabase": "^1.178.2",
"supabase": "^1.167.4",
"svelte": "^4.2.8",
"svelte-check": "^3.6.2",
"tailwindcss": "^3.4.3",
@@ -44,8 +42,8 @@
},
"type": "module",
"dependencies": {
"@supabase/ssr": "^0.4.0",
"@supabase/supabase-js": "^2.44.2",
"@supabase/ssr": "^0.1.0",
"@supabase/supabase-js": "^2.42.0",
"stripe": "^15.4.0"
},
"engines": {
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
plugins: [require('tailwindcss'), require('autoprefixer')],
};
+5 -5
View File
@@ -2,22 +2,22 @@
// for information about these interfaces
// and what to do when importing types
import { SupabaseClient, Session, User } from '@supabase/supabase-js';
import type { Database } from '$types/supabase';
import { Database } from '$lib/db_schema';
declare global {
const __DATE__: string;
const __RELOAD_SW__: boolean;
namespace App {
// interface Error {}
interface Locals {
supabase: SupabaseClient<Database>;
safeGetSession(): Promise<{ session: Session | null; user: User | null }>;
supabase: SupabaseClient<Database>
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
userid: string;
buildDate: string;
periodicUpdates: boolean;
}
interface PageData {
session: Session | null;
}
session: Session | null
}
// interface PageState {}
// interface Platform {}
}
+1
View File
@@ -5,6 +5,7 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
<link rel="stylesheet" href="%sveltekit.assets%/output.css" />
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
-1
View File
@@ -1 +0,0 @@
/* Write your global styles here, in PostCSS syntax */
+17 -20
View File
@@ -5,48 +5,45 @@ import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
getAll: () => event.cookies.getAll(),
get: (key) => event.cookies.get(key),
/**
* SvelteKit's cookies API requires `path` to be explicitly set in
* the cookie options. Setting `path` to `/` replicates previous/
* standard behavior.
* Note: You have to add the `path` variable to the
* set and remove method due to sveltekit's cookie API
* requiring this to be set, setting the path to an empty string
* will replicate previous/standard behaviour (https://kit.svelte.dev/docs/types#public-types-cookies)
*/
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
event.cookies.set(name, value, { ...options, path: '/' });
});
set: (key, value, options) => {
event.cookies.set(key, value, { ...options, path: '/' });
},
remove: (key, options) => {
event.cookies.delete(key, { ...options, path: '/' });
}
}
});
/**
* Unlike `supabase.auth.getSession()`, which returns the session _without_
* validating the JWT, this function also calls `getUser()` to validate the
* JWT before returning the session.
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
* doesn't validate the JWT, this function validates the JWT by first calling
* `getUser` and aborts early if the JWT signature is invalid.
*/
event.locals.safeGetSession = async () => {
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
if (!session) {
return { session: null, user: null };
}
const {
data: { user },
error
} = await event.locals.supabase.auth.getUser();
if (error) {
// JWT validation has failed
return { session: null, user: null };
}
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
return { session, user };
};
return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range' || name === 'x-supabase-api-version';
return name === 'content-range';
}
});
};
+1 -21
View File
@@ -1,15 +1,10 @@
<script lang="ts">
import stripe from 'stripe';
import { PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public';
let email = '';
let password = '';
// Access the supabase client from the layout data
export let supabase: any;
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
async function signUpNewUser() {
try {
const { data, error } = await supabase.auth.signUp({
@@ -17,7 +12,7 @@
password: password,
options: {
// Redirect URL after successful sign-up
redirectTo: '/account'
redirectTo: '/welcome'
}
});
@@ -25,21 +20,6 @@
throw error;
}
const customer = await stripeClient.customers.create({
email
});
const response = await supabase
.from('user')
.update({
customer_id: customer.id
})
.match({
id: data.user.id
});
console.log(response);
// Handle success (optional)
} catch (error: any) {
console.error('Sign up error:', error.message);
+345
View File
@@ -0,0 +1,345 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = {
graphql_public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
graphql: {
Args: {
operationName?: string
query?: string
variables?: Json
extensions?: Json
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
profiles: {
Row: {
created_at: string | null
full_name: string | null
id: string
updated_at: string | null
username: string | null
}
Insert: {
created_at?: string | null
full_name?: string | null
id: string
updated_at?: string | null
username?: string | null
}
Update: {
created_at?: string | null
full_name?: string | null
id?: string
updated_at?: string | null
username?: string | null
}
Relationships: [
{
foreignKeyName: "profiles_id_fkey"
columns: ["id"]
isOneToOne: true
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
storage: {
Tables: {
buckets: {
Row: {
allowed_mime_types: string[] | null
avif_autodetection: boolean | null
created_at: string | null
file_size_limit: number | null
id: string
name: string
owner: string | null
owner_id: string | null
public: boolean | null
updated_at: string | null
}
Insert: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id: string
name: string
owner?: string | null
owner_id?: string | null
public?: boolean | null
updated_at?: string | null
}
Update: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id?: string
name?: string
owner?: string | null
owner_id?: string | null
public?: boolean | null
updated_at?: string | null
}
Relationships: []
}
migrations: {
Row: {
executed_at: string | null
hash: string
id: number
name: string
}
Insert: {
executed_at?: string | null
hash: string
id: number
name: string
}
Update: {
executed_at?: string | null
hash?: string
id?: number
name?: string
}
Relationships: []
}
objects: {
Row: {
bucket_id: string | null
created_at: string | null
id: string
last_accessed_at: string | null
metadata: Json | null
name: string | null
owner: string | null
owner_id: string | null
path_tokens: string[] | null
updated_at: string | null
version: string | null
}
Insert: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
owner_id?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Update: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
owner_id?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Relationships: [
{
foreignKeyName: "objects_bucketId_fkey"
columns: ["bucket_id"]
isOneToOne: false
referencedRelation: "buckets"
referencedColumns: ["id"]
},
]
}
}
Views: {
[_ in never]: never
}
Functions: {
can_insert_object: {
Args: {
bucketid: string
name: string
owner: string
metadata: Json
}
Returns: undefined
}
extension: {
Args: {
name: string
}
Returns: string
}
filename: {
Args: {
name: string
}
Returns: string
}
foldername: {
Args: {
name: string
}
Returns: string[]
}
get_size_by_bucket: {
Args: Record<PropertyKey, never>
Returns: {
size: number
bucket_id: string
}[]
}
search: {
Args: {
prefix: string
bucketname: string
limits?: number
levels?: number
offsets?: number
search?: string
sortcolumn?: string
sortorder?: string
}
Returns: {
name: string
id: string
updated_at: string
created_at: string
last_accessed_at: string
metadata: Json
}[]
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
type PublicSchema = Database[Extract<keyof Database, "public">]
export type Tables<
PublicTableNameOrOptions extends
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
PublicSchema["Views"])
? (PublicSchema["Tables"] &
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
PublicEnumNameOrOptions extends
| keyof PublicSchema["Enums"]
| { schema: keyof Database },
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = PublicEnumNameOrOptions extends { schema: keyof Database }
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
? PublicSchema["Enums"][PublicEnumNameOrOptions]
: never
-251
View File
@@ -1,251 +0,0 @@
import { createClient } from '@supabase/supabase-js';
import stripe from 'stripe';
import type { Database, Tables, TablesInsert } from '$types/supabase';
import { PUBLIC_SUPABASE_URL, PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public';
import { SUPABASE_SERVICE_ROLE_KEY } from '$env/static/private';
const toDateTime = (secs: number) => {
const t = new Date(+0); // Unix epoch start.
t.setSeconds(secs);
return t;
};
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
type Product = Tables<'products'>;
type Price = Tables<'prices'>;
// Change to control trial period length
const TRIAL_PERIOD_DAYS = 0;
// Note: supabaseAdmin uses the SERVICE_ROLE_KEY which you must only use in a secure server-side context
// as it has admin privileges and overwrites RLS policies!
const supabaseAdmin = createClient<Database>(
PUBLIC_SUPABASE_URL || '',
SUPABASE_SERVICE_ROLE_KEY || ''
);
const upsertProductRecord = async (product: stripe.Product) => {
const productData: Product = {
id: product.id,
active: product.active,
name: product.name,
description: product.description ?? null,
image: product.images?.[0] ?? null,
metadata: product.metadata
};
const { error: upsertError } = await supabaseAdmin.from('products').upsert([productData]);
if (upsertError) throw new Error(`Product insert/update failed: ${upsertError.message}`);
console.log(`Product inserted/updated: ${product.id}`);
};
const upsertPriceRecord = async (price: stripe.Price, retryCount = 0, maxRetries = 3) => {
const priceData: Price = {
id: price.id,
product_id: typeof price.product === 'string' ? price.product : '',
active: price.active,
currency: price.currency,
type: price.type,
unit_amount: price.unit_amount ?? null,
interval: price.recurring?.interval ?? null,
interval_count: price.recurring?.interval_count ?? null,
trial_period_days: price.recurring?.trial_period_days ?? TRIAL_PERIOD_DAYS
};
const { error: upsertError } = await supabaseAdmin.from('prices').upsert([priceData]);
if (upsertError?.message.includes('foreign key constraint')) {
if (retryCount < maxRetries) {
console.log(`Retry attempt ${retryCount + 1} for price ID: ${price.id}`);
await new Promise((resolve) => setTimeout(resolve, 2000));
await upsertPriceRecord(price, retryCount + 1, maxRetries);
} else {
throw new Error(
`Price insert/update failed after ${maxRetries} retries: ${upsertError.message}`
);
}
} else if (upsertError) {
throw new Error(`Price insert/update failed: ${upsertError.message}`);
} else {
console.log(`Price inserted/updated: ${price.id}`);
}
};
const deleteProductRecord = async (product: stripe.Product) => {
const { error: deletionError } = await supabaseAdmin
.from('products')
.delete()
.eq('id', product.id);
if (deletionError) throw new Error(`Product deletion failed: ${deletionError.message}`);
console.log(`Product deleted: ${product.id}`);
};
const deletePriceRecord = async (price: stripe.Price) => {
const { error: deletionError } = await supabaseAdmin.from('prices').delete().eq('id', price.id);
if (deletionError) throw new Error(`Price deletion failed: ${deletionError.message}`);
console.log(`Price deleted: ${price.id}`);
};
const upsertCustomerToSupabase = async (uuid: string, customerId: string) => {
const { error: upsertError } = await supabaseAdmin
.from('customers')
.upsert([{ id: uuid, stripe_customer_id: customerId }]);
if (upsertError)
throw new Error(`Supabase customer record creation failed: ${upsertError.message}`);
return customerId;
};
const createCustomerInStripe = async (uuid: string, email: string) => {
const customerData = { metadata: { supabaseUUID: uuid }, email: email };
const newCustomer = await stripeClient.customers.create(customerData);
if (!newCustomer) throw new Error('Stripe customer creation failed.');
return newCustomer.id;
};
const createOrRetrieveCustomer = async ({ email, uuid }: { email: string; uuid: string }) => {
// Check if the customer already exists in Supabase
const { data: existingSupabaseCustomer, error: queryError } = await supabaseAdmin
.from('customers')
.select('*')
.eq('id', uuid)
.maybeSingle();
if (queryError) {
throw new Error(`Supabase customer lookup failed: ${queryError.message}`);
}
// Retrieve the Stripe customer ID using the Supabase customer ID, with email fallback
let stripeCustomerId: string | undefined;
if (existingSupabaseCustomer?.stripe_customer_id) {
const existingStripeCustomer = await stripeClient.customers.retrieve(
existingSupabaseCustomer.stripe_customer_id
);
stripeCustomerId = existingStripeCustomer.id;
} else {
// If Stripe ID is missing from Supabase, try to retrieve Stripe customer ID by email
const stripeCustomers = await stripeClient.customers.list({ email: email });
stripeCustomerId = stripeCustomers.data.length > 0 ? stripeCustomers.data[0].id : undefined;
}
// If still no stripeCustomerId, create a new customer in Stripe
const stripeIdToInsert = stripeCustomerId
? stripeCustomerId
: await createCustomerInStripe(uuid, email);
if (!stripeIdToInsert) throw new Error('Stripe customer creation failed.');
if (existingSupabaseCustomer && stripeCustomerId) {
// If Supabase has a record but doesn't match Stripe, update Supabase record
if (existingSupabaseCustomer.stripe_customer_id !== stripeCustomerId) {
const { error: updateError } = await supabaseAdmin
.from('customers')
.update({ stripe_customer_id: stripeCustomerId })
.eq('id', uuid);
if (updateError)
throw new Error(`Supabase customer record update failed: ${updateError.message}`);
console.warn(`Supabase customer record mismatched Stripe ID. Supabase record updated.`);
}
// If Supabase has a record and matches Stripe, return Stripe customer ID
return stripeCustomerId;
} else {
console.warn(`Supabase customer record was missing. A new record was created.`);
// If Supabase has no record, create a new record and return Stripe customer ID
const upsertedStripeCustomer = await upsertCustomerToSupabase(uuid, stripeIdToInsert);
if (!upsertedStripeCustomer) throw new Error('Supabase customer record creation failed.');
return upsertedStripeCustomer;
}
};
/**
* Copies the billing details from the payment method to the customer object.
*/
const copyBillingDetailsToCustomer = async (uuid: string, payment_method: stripe.PaymentMethod) => {
//Todo: check this assertion
const customer = payment_method.customer as string;
const { name, phone, address } = payment_method.billing_details;
if (!name || !phone || !address) return;
await stripeClient.customers.update(customer, { name, phone, address });
const { error: updateError } = await supabaseAdmin
.from('users')
.update({
billing_address: { ...address },
payment_method: { ...payment_method[payment_method.type] }
})
.eq('id', uuid);
if (updateError) throw new Error(`Customer update failed: ${updateError.message}`);
};
const manageSubscriptionStatusChange = async (
subscriptionId: string,
customerId: string,
createAction = false
) => {
// Get customer's UUID from mapping table.
const { data: customerData, error: noCustomerError } = await supabaseAdmin
.from('customers')
.select('id')
.eq('stripe_customer_id', customerId)
.single();
if (noCustomerError) throw new Error(`Customer lookup failed: ${noCustomerError.message}`);
const { id: uuid } = customerData!;
const subscription = await stripeClient.subscriptions.retrieve(subscriptionId, {
expand: ['default_payment_method']
});
// Upsert the latest status of the subscription object.
const subscriptionData: TablesInsert<'subscriptions'> = {
id: subscription.id,
user_id: uuid,
metadata: subscription.metadata,
status: subscription.status,
price_id: subscription.items.data[0].price.id,
//TODO check quantity on subscription
quantity: subscription.quantity,
cancel_at_period_end: subscription.cancel_at_period_end,
cancel_at: subscription.cancel_at ? toDateTime(subscription.cancel_at).toISOString() : null,
canceled_at: subscription.canceled_at
? toDateTime(subscription.canceled_at).toISOString()
: null,
current_period_start: toDateTime(subscription.current_period_start).toISOString(),
current_period_end: toDateTime(subscription.current_period_end).toISOString(),
created: toDateTime(subscription.created).toISOString(),
ended_at: subscription.ended_at ? toDateTime(subscription.ended_at).toISOString() : null,
trial_start: subscription.trial_start
? toDateTime(subscription.trial_start).toISOString()
: null,
trial_end: subscription.trial_end ? toDateTime(subscription.trial_end).toISOString() : null
};
const { error: upsertError } = await supabaseAdmin
.from('subscriptions')
.upsert([subscriptionData]);
if (upsertError) throw new Error(`Subscription insert/update failed: ${upsertError.message}`);
console.log(`Inserted/updated subscription [${subscription.id}] for user [${uuid}]`);
// For a new subscription copy the billing details to the customer object.
// NOTE: This is a costly operation and should happen at the very end.
if (createAction && subscription.default_payment_method && uuid)
await copyBillingDetailsToCustomer(
uuid,
subscription.default_payment_method as stripe.PaymentMethod
);
};
export {
upsertProductRecord,
upsertPriceRecord,
deleteProductRecord,
deletePriceRecord,
createOrRetrieveCustomer,
manageSubscriptionStatusChange
};
+2 -3
View File
@@ -1,11 +1,10 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
const { session, user } = await safeGetSession();
return {
session,
user,
cookies: cookies.getAll()
user
};
};
+5 -5
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import '../app.postcss';
import 'tailwindcss/tailwind.css';
import { onDestroy, onMount } from 'svelte';
import { isBrowser } from '@supabase/ssr';
import { basket, type Basket, type Item } from '$lib/stores/basket.js';
@@ -32,8 +32,8 @@
let cookiesAccepted = false;
onMount(() => {
const { data } = supabase.auth.onAuthStateChange((event, newSession) => {
if (newSession?.expires_at !== session?.expires_at) {
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
if (_session?.expires_at !== session?.expires_at) {
// tells SvelteKit that the root +layout.ts load function should be executed whenever the session updates to keep the page store in sync.
invalidate('supabase:auth');
}
@@ -138,7 +138,7 @@
class="mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-base-100 text-base-content rounded-box min-w-[13rem] w-auto"
>
<li>
<a href="/account"> Account </a>
<a href="/profile"> Profile </a>
</li>
<li><a href="/settings">Settings</a></li>
<li><SignOut {supabase} /></li>
@@ -265,7 +265,7 @@
<span>{localGeneral.toastMessage == '' ? 'Added to basket' : localGeneral.toastMessage}</span>
</div>
<style scoped lang="postcss">
<style scoped>
.user-circle {
width: 40px;
height: 40px;
+15 -16
View File
@@ -1,26 +1,25 @@
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr';
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public';
import type { LayoutLoad } from './$types';
import { createBrowserClient, isBrowser, parse } from '@supabase/ssr';
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
depends('supabase:auth');
const supabase = isBrowser()
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch
},
cookies: {
get(key) {
if (!isBrowser()) {
return JSON.stringify(data.session);
}
})
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch
},
cookies: {
getAll() {
return data.cookies;
}
}
});
const cookie = parse(document.cookie);
return cookie[key];
}
}
});
/**
* It's fine to use `getSession` here, because on the client, `getSession` is
+2
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { type User } from '@supabase/auth-js';
import SignUp from '$lib/components/SignUp.svelte';
export let data;
-55
View File
@@ -1,55 +0,0 @@
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
const { session } = await safeGetSession();
if (!session) {
redirect(303, '/');
}
const { data: profile } = await supabase
.from('profiles')
.select(`username, full_name`)
.eq('id', session.user.id)
.single();
return { session, profile };
};
export const actions: Actions = {
update: async ({ request, locals: { supabase, safeGetSession } }) => {
const formData = await request.formData();
const fullName = formData.get('fullName') as string;
const { session } = await safeGetSession();
const { error } = await supabase.from('profiles').upsert({
id: session?.user.id,
full_name: fullName,
username: session?.user.email,
updated_at: new Date()
});
console.error(error);
if (error) {
return fail(500, {
fullName,
username: session?.user.email
});
}
return {
fullName,
username: session?.user.email
};
},
signout: async ({ locals: { supabase, safeGetSession } }) => {
const { session } = await safeGetSession();
if (session) {
await supabase.auth.signOut();
redirect(303, '/');
}
}
};
-64
View File
@@ -1,64 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { SubmitFunction } from '@sveltejs/kit';
export let data;
export let form;
let { session, supabase, profile } = data;
$: ({ session, supabase, profile } = data);
let profileForm: HTMLFormElement;
let loading = false;
let fullName: string = profile?.full_name ?? '';
const handleSubmit: SubmitFunction = () => {
loading = true;
return async () => {
loading = false;
};
};
const handleSignOut: SubmitFunction = () => {
loading = true;
return async ({ update }) => {
loading = false;
update();
};
};
</script>
<div class="form-widget">
<form
class="form-widget"
method="post"
action="?/update"
use:enhance={handleSubmit}
bind:this={profileForm}
>
<div>
<label for="email">Username</label>
<input id="email" type="text" value={session.user.email} disabled />
</div>
<div>
<label for="fullName">Full Name</label>
<input id="fullName" name="fullName" type="text" value={form?.fullName ?? fullName} />
</div>
<div>
<input
type="submit"
class="button block primary"
value={loading ? 'Loading...' : 'Update'}
disabled={loading}
/>
</div>
</form>
<form method="post" action="?/signout" use:enhance={handleSignOut}>
<div>
<button class="button block" disabled={loading}>Sign Out</button>
</div>
</form>
</div>
-95
View File
@@ -1,95 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import stripe from 'stripe';
import { PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public';
import { PUBLIC_STRIPE_ENDPOINT_SECRET } from '$env/static/public';
import {
deletePriceRecord,
deleteProductRecord,
manageSubscriptionStatusChange,
upsertPriceRecord,
upsertProductRecord
} from '$lib/utils/supabase/admin';
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
export const POST: RequestHandler = async ({ request }) => {
const body = await request.text();
try {
let event = JSON.parse(body) as stripe.Event;
// Get the signature sent by Stripe
const signature = request.headers.get('stripe-signature');
// Only verify the event if you have an endpoint secret defined.
// Otherwise use the basic event deserialized with JSON.parse
if (PUBLIC_STRIPE_ENDPOINT_SECRET && signature) {
try {
event = stripeClient.webhooks.constructEvent(
body,
signature,
PUBLIC_STRIPE_ENDPOINT_SECRET
);
} catch (err) {
console.error(`⚠️ Webhook signature verification failed.`, err.message);
return {
status: 400,
body: {}
};
}
}
switch (event.type) {
case 'product.created':
case 'product.updated':
await upsertProductRecord(event.data.object as stripe.Product);
break;
case 'price.created':
case 'price.updated':
await upsertPriceRecord(event.data.object as stripe.Price);
break;
case 'price.deleted':
await deletePriceRecord(event.data.object as stripe.Price);
break;
case 'product.deleted':
await deleteProductRecord(event.data.object as stripe.Product);
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
case 'customer.subscription.deleted': {
const subscription = event.data.object as stripe.Subscription;
await manageSubscriptionStatusChange(
subscription.id,
subscription.customer as string,
event.type === 'customer.subscription.created'
);
break;
}
case 'checkout.session.completed': {
const checkoutSession = event.data.object as stripe.Checkout.Session;
if (checkoutSession.mode === 'subscription') {
const subscriptionId = checkoutSession.subscription;
await manageSubscriptionStatusChange(
subscriptionId as string,
checkoutSession.customer as string,
true
);
}
break;
}
default:
console.error(`Unhandled event type ${event.type}`);
}
// Return a response to acknowledge receipt of the event
return new Response(JSON.stringify({ received: true }), {
status: 200
});
} catch (err) {
console.log(`Webhook Error: ${err.message}`);
return {
status: 400,
body: `Webhook Error: ${err.message}`
};
}
};
-1
View File
@@ -1 +0,0 @@
<p>Login error</p>
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { type User } from '@supabase/auth-js';
export let data;
let { supabase, session } = data;
$: ({ supabase, session } = data);
</script>
<div class="h-screen">
{#if session?.user}
{session?.user?.email}
{:else}
<p>Please log in to view this page</p>
{/if}
</div>
-1
View File
@@ -1,4 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
+19 -52
View File
@@ -5,11 +5,11 @@ project_id = "SvelteKitSaasBoilerplate"
[api]
enabled = true
# Port to use for the API URL.
port = 54321
port = 55321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` is always included.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request. `public` is always included.
# 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.
@@ -17,9 +17,9 @@ max_rows = 1000
[db]
# Port to use for the local database URL.
port = 54322
port = 55322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
shadow_port = 55320
# 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
@@ -27,7 +27,7 @@ major_version = 15
[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
port = 55329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
@@ -38,7 +38,7 @@ max_client_conn = 100
[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# Bind realtime via either IPv4 or IPv6. (default: IPv6)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096
@@ -46,7 +46,7 @@ enabled = true
[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
port = 55323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
@@ -57,26 +57,23 @@ openai_api_key = "env(OPENAI_API_KEY)"
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
port = 55324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326
# smtp_port = 55325
# pop3_port = 55326
[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"
[storage.image_transformation]
enabled = true
[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
site_url = "http://localhost:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
additional_redirect_urls = ["http://localhost:3000/**"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
@@ -86,8 +83,6 @@ enable_refresh_token_rotation = true
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
@@ -99,17 +94,6 @@ enable_signup = true
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Use a production-ready SMTP server
# [auth.email.smtp]
# host = "smtp.sendgrid.net"
# port = 587
# user = "apikey"
# pass = "env(SENDGRID_API_KEY)"
# admin_email = "admin@email.com"
# sender_name = "Admin"
# Uncomment to customize email template
# [auth.email.template.invite]
@@ -123,25 +107,17 @@ enable_signup = true
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }} ."
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"
# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
[auth.sms.test_otp]
# 4152127777 = "123456"
# Configure logged in session timeouts.
# [auth.sessions]
# Force log out after the specified duration.
# timebox = "24h"
# Force log out if the user has been inactive longer than the specified duration.
# inactivity_timeout = "8h"
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
[auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
@@ -163,20 +139,11 @@ 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 = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
[edge_runtime]
enabled = true
# Configure one of the supported request policies: `oneshot`, `per_worker`.
# Use `oneshot` for hot reload, or `per_worker` for load testing.
policy = "oneshot"
inspector_port = 8083
[analytics]
enabled = false
port = 54327
vector_port = 54328
port = 55327
vector_port = 55328
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"
@@ -0,0 +1,109 @@
create table "public"."profiles" (
"id" uuid not null,
"updated_at" timestamp with time zone,
"created_at" timestamp with time zone default now(),
"username" text,
"full_name" text
);
alter table "public"."profiles" enable row level security;
CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id);
CREATE UNIQUE INDEX profiles_username_key ON public.profiles USING btree (username);
alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey";
alter table "public"."profiles" add constraint "profiles_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
alter table "public"."profiles" validate constraint "profiles_id_fkey";
alter table "public"."profiles" add constraint "profiles_username_key" UNIQUE using index "profiles_username_key";
alter table "public"."profiles" add constraint "username_length" CHECK ((char_length(username) >= 3)) not valid;
alter table "public"."profiles" validate constraint "username_length";
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
begin
insert into public.profiles (id, full_name)
values (new.id, new.raw_user_meta_data->>'full_name');
return new;
end;
$function$
;
grant delete on table "public"."profiles" to "anon";
grant insert on table "public"."profiles" to "anon";
grant references on table "public"."profiles" to "anon";
grant select on table "public"."profiles" to "anon";
grant trigger on table "public"."profiles" to "anon";
grant truncate on table "public"."profiles" to "anon";
grant update on table "public"."profiles" to "anon";
grant delete on table "public"."profiles" to "authenticated";
grant insert on table "public"."profiles" to "authenticated";
grant references on table "public"."profiles" to "authenticated";
grant select on table "public"."profiles" to "authenticated";
grant trigger on table "public"."profiles" to "authenticated";
grant truncate on table "public"."profiles" to "authenticated";
grant update on table "public"."profiles" to "authenticated";
grant delete on table "public"."profiles" to "service_role";
grant insert on table "public"."profiles" to "service_role";
grant references on table "public"."profiles" to "service_role";
grant select on table "public"."profiles" to "service_role";
grant trigger on table "public"."profiles" to "service_role";
grant truncate on table "public"."profiles" to "service_role";
grant update on table "public"."profiles" to "service_role";
create policy "Public profiles are viewable by everyone."
on "public"."profiles"
as permissive
for select
to public
using (true);
create policy "Users can insert their own profile."
on "public"."profiles"
as permissive
for insert
to public
with check ((auth.uid() = id));
create policy "Users can update own profile."
on "public"."profiles"
as permissive
for update
to public
using ((auth.uid() = id));
@@ -0,0 +1,4 @@
-- 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 public.handle_new_user();
@@ -1,159 +0,0 @@
/**
* USERS
* Note: This table contains user data. Users should only be able to view and update their own data.
*/
create table users (
-- UUID from auth.users
id uuid references auth.users not null primary key,
full_name text,
avatar_url text,
-- The customer's billing address, stored in JSON format.
billing_address jsonb,
-- Stores your customer's payment instruments.
payment_method jsonb
);
alter table users
enable row level security;
create policy "Can view own user data." on users
for select using ((select auth.uid()) = id);
create policy "Can update own user data." on users
for update using ((select auth.uid()) = id);
/**
* This trigger automatically creates a user entry when a new user signs up via Supabase Auth.
*/
create function public.handle_new_user()
returns trigger as
$$
begin
insert into public.users (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$
language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row
execute procedure public.handle_new_user();
/**
* CUSTOMERS
* Note: this is a private table that contains a mapping of user IDs to Stripe customer IDs.
*/
create table customers (
-- UUID from auth.users
id uuid references auth.users not null primary key,
-- The user's customer ID in Stripe. User must not be able to update this.
stripe_customer_id text
);
alter table customers enable row level security;
-- No policies as this is a private table that the user must not have access to.
/**
* PRODUCTS
* Note: products are created and managed in Stripe and synced to our DB via Stripe webhooks.
*/
create table products (
-- Product ID from Stripe, e.g. prod_1234.
id text primary key,
-- Whether the product is currently available for purchase.
active boolean,
-- The product's name, meant to be displayable to the customer. Whenever this product is sold via a subscription, name will show up on associated invoice line item descriptions.
name text,
-- The product's description, meant to be displayable to the customer. Use this field to optionally store a long form explanation of the product being sold for your own rendering purposes.
description text,
-- A URL of the product image in Stripe, meant to be displayable to the customer.
image text,
-- Set of key-value pairs, used to store additional information about the object in a structured format.
metadata jsonb
);
alter table products
enable row level security;
create policy "Allow public read-only access." on products
for select using (true);
/**
* PRICES
* Note: prices are created and managed in Stripe and synced to our DB via Stripe webhooks.
*/
create type pricing_type as enum ('one_time', 'recurring');
create type pricing_plan_interval as enum ('day', 'week', 'month', 'year');
create table prices (
-- Price ID from Stripe, e.g. price_1234.
id text primary key,
-- The ID of the prduct that this price belongs to.
product_id text references products,
-- Whether the price can be used for new purchases.
active boolean,
-- A brief description of the price.
description text,
-- The unit amount as a positive integer in the smallest currency unit (e.g., 100 cents for US$1.00 or 100 for ¥100, a zero-decimal currency).
unit_amount bigint,
-- Three-letter ISO currency code, in lowercase.
currency text check (char_length(currency) = 3),
-- One of `one_time` or `recurring` depending on whether the price is for a one-time purchase or a recurring (subscription) purchase.
type pricing_type,
-- The frequency at which a subscription is billed. One of `day`, `week`, `month` or `year`.
interval pricing_plan_interval,
-- The number of intervals (specified in the `interval` attribute) between subscription billings. For example, `interval=month` and `interval_count=3` bills every 3 months.
interval_count integer,
-- Default number of trial days when subscribing a customer to this price using [`trial_from_plan=true`](https://stripe.com/docs/api#create_subscription-trial_from_plan).
trial_period_days integer,
-- Set of key-value pairs, used to store additional information about the object in a structured format.
metadata jsonb
);
alter table prices
enable row level security;
create policy "Allow public read-only access." on prices
for select using (true);
/**
* SUBSCRIPTIONS
* Note: subscriptions are created and managed in Stripe and synced to our DB via Stripe webhooks.
*/
create type subscription_status as enum ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid');
create table subscriptions (
-- Subscription ID from Stripe, e.g. sub_1234.
id text primary key,
user_id uuid references auth.users not null,
-- The status of the subscription object, one of subscription_status type above.
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 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.
quantity integer,
-- If true the subscription has been canceled by the user and will be deleted at the end of the billing period.
cancel_at_period_end boolean,
-- Time at which the subscription was created.
created timestamp with time zone default timezone('utc'::text, now()) not null,
-- Start of the current period that the subscription has been invoiced for.
current_period_start timestamp with time zone default timezone('utc'::text, now()) not null,
-- End of the current period that the subscription has been invoiced for. At the end of this period, a new invoice will be created.
current_period_end timestamp with time zone default timezone('utc'::text, now()) not null,
-- If the subscription has ended, the timestamp of the date the subscription ended.
ended_at timestamp with time zone default timezone('utc'::text, now()),
-- A date in the future at which the subscription will automatically get canceled.
cancel_at timestamp with time zone default timezone('utc'::text, now()),
-- If the subscription has been canceled, the date of that cancellation. If the subscription was canceled with `cancel_at_period_end`, `canceled_at` will still reflect the date of the initial cancellation request, not the end of the subscription period when the subscription is automatically moved to a canceled state.
canceled_at timestamp with time zone default timezone('utc'::text, now()),
-- If the subscription has a trial, the beginning of that trial.
trial_start timestamp with time zone default timezone('utc'::text, now()),
-- If the subscription has a trial, the end of that trial.
trial_end timestamp with time zone default timezone('utc'::text, now())
);
alter table subscriptions
enable row level security;
create policy "Can only view own subs data." on subscriptions
for select using ((select auth.uid()) = user_id);
/**
* REALTIME SUBSCRIPTIONS
* Only allow realtime listening on public tables.
*/
drop publication if exists supabase_realtime;
create publication supabase_realtime
for table products, prices;
-653
View File
@@ -1,653 +0,0 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = {
graphql_public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
graphql: {
Args: {
operationName?: string
query?: string
variables?: Json
extensions?: Json
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
customers: {
Row: {
id: string
stripe_customer_id: string | null
}
Insert: {
id: string
stripe_customer_id?: string | null
}
Update: {
id?: string
stripe_customer_id?: string | null
}
Relationships: [
{
foreignKeyName: "customers_id_fkey"
columns: ["id"]
isOneToOne: true
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
prices: {
Row: {
active: boolean | null
currency: string | null
description: string | null
id: string
interval: Database["public"]["Enums"]["pricing_plan_interval"] | null
interval_count: number | null
metadata: Json | null
product_id: string | null
trial_period_days: number | null
type: Database["public"]["Enums"]["pricing_type"] | null
unit_amount: number | null
}
Insert: {
active?: boolean | null
currency?: string | null
description?: string | null
id: string
interval?: Database["public"]["Enums"]["pricing_plan_interval"] | null
interval_count?: number | null
metadata?: Json | null
product_id?: string | null
trial_period_days?: number | null
type?: Database["public"]["Enums"]["pricing_type"] | null
unit_amount?: number | null
}
Update: {
active?: boolean | null
currency?: string | null
description?: string | null
id?: string
interval?: Database["public"]["Enums"]["pricing_plan_interval"] | null
interval_count?: number | null
metadata?: Json | null
product_id?: string | null
trial_period_days?: number | null
type?: Database["public"]["Enums"]["pricing_type"] | null
unit_amount?: number | null
}
Relationships: [
{
foreignKeyName: "prices_product_id_fkey"
columns: ["product_id"]
isOneToOne: false
referencedRelation: "products"
referencedColumns: ["id"]
},
]
}
products: {
Row: {
active: boolean | null
description: string | null
id: string
image: string | null
metadata: Json | null
name: string | null
}
Insert: {
active?: boolean | null
description?: string | null
id: string
image?: string | null
metadata?: Json | null
name?: string | null
}
Update: {
active?: boolean | null
description?: string | null
id?: string
image?: string | null
metadata?: Json | null
name?: string | null
}
Relationships: []
}
subscriptions: {
Row: {
cancel_at: string | null
cancel_at_period_end: boolean | null
canceled_at: string | null
created: string
current_period_end: string
current_period_start: string
ended_at: string | null
id: string
metadata: Json | null
price_id: string | null
quantity: number | null
status: Database["public"]["Enums"]["subscription_status"] | null
trial_end: string | null
trial_start: string | null
user_id: string
}
Insert: {
cancel_at?: string | null
cancel_at_period_end?: boolean | null
canceled_at?: string | null
created?: string
current_period_end?: string
current_period_start?: string
ended_at?: string | null
id: string
metadata?: Json | null
price_id?: string | null
quantity?: number | null
status?: Database["public"]["Enums"]["subscription_status"] | null
trial_end?: string | null
trial_start?: string | null
user_id: string
}
Update: {
cancel_at?: string | null
cancel_at_period_end?: boolean | null
canceled_at?: string | null
created?: string
current_period_end?: string
current_period_start?: string
ended_at?: string | null
id?: string
metadata?: Json | null
price_id?: string | null
quantity?: number | null
status?: Database["public"]["Enums"]["subscription_status"] | null
trial_end?: string | null
trial_start?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: "subscriptions_price_id_fkey"
columns: ["price_id"]
isOneToOne: false
referencedRelation: "prices"
referencedColumns: ["id"]
},
{
foreignKeyName: "subscriptions_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
users: {
Row: {
avatar_url: string | null
billing_address: Json | null
full_name: string | null
id: string
payment_method: Json | null
}
Insert: {
avatar_url?: string | null
billing_address?: Json | null
full_name?: string | null
id: string
payment_method?: Json | null
}
Update: {
avatar_url?: string | null
billing_address?: Json | null
full_name?: string | null
id?: string
payment_method?: Json | null
}
Relationships: [
{
foreignKeyName: "users_id_fkey"
columns: ["id"]
isOneToOne: true
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
pricing_plan_interval: "day" | "week" | "month" | "year"
pricing_type: "one_time" | "recurring"
subscription_status:
| "trialing"
| "active"
| "canceled"
| "incomplete"
| "incomplete_expired"
| "past_due"
| "unpaid"
}
CompositeTypes: {
[_ in never]: never
}
}
storage: {
Tables: {
buckets: {
Row: {
allowed_mime_types: string[] | null
avif_autodetection: boolean | null
created_at: string | null
file_size_limit: number | null
id: string
name: string
owner: string | null
owner_id: string | null
public: boolean | null
updated_at: string | null
}
Insert: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id: string
name: string
owner?: string | null
owner_id?: string | null
public?: boolean | null
updated_at?: string | null
}
Update: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id?: string
name?: string
owner?: string | null
owner_id?: string | null
public?: boolean | null
updated_at?: string | null
}
Relationships: []
}
migrations: {
Row: {
executed_at: string | null
hash: string
id: number
name: string
}
Insert: {
executed_at?: string | null
hash: string
id: number
name: string
}
Update: {
executed_at?: string | null
hash?: string
id?: number
name?: string
}
Relationships: []
}
objects: {
Row: {
bucket_id: string | null
created_at: string | null
id: string
last_accessed_at: string | null
metadata: Json | null
name: string | null
owner: string | null
owner_id: string | null
path_tokens: string[] | null
updated_at: string | null
version: string | null
}
Insert: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
owner_id?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Update: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
owner_id?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Relationships: [
{
foreignKeyName: "objects_bucketId_fkey"
columns: ["bucket_id"]
isOneToOne: false
referencedRelation: "buckets"
referencedColumns: ["id"]
},
]
}
s3_multipart_uploads: {
Row: {
bucket_id: string
created_at: string
id: string
in_progress_size: number
key: string
owner_id: string | null
upload_signature: string
version: string
}
Insert: {
bucket_id: string
created_at?: string
id: string
in_progress_size?: number
key: string
owner_id?: string | null
upload_signature: string
version: string
}
Update: {
bucket_id?: string
created_at?: string
id?: string
in_progress_size?: number
key?: string
owner_id?: string | null
upload_signature?: string
version?: string
}
Relationships: [
{
foreignKeyName: "s3_multipart_uploads_bucket_id_fkey"
columns: ["bucket_id"]
isOneToOne: false
referencedRelation: "buckets"
referencedColumns: ["id"]
},
]
}
s3_multipart_uploads_parts: {
Row: {
bucket_id: string
created_at: string
etag: string
id: string
key: string
owner_id: string | null
part_number: number
size: number
upload_id: string
version: string
}
Insert: {
bucket_id: string
created_at?: string
etag: string
id?: string
key: string
owner_id?: string | null
part_number: number
size?: number
upload_id: string
version: string
}
Update: {
bucket_id?: string
created_at?: string
etag?: string
id?: string
key?: string
owner_id?: string | null
part_number?: number
size?: number
upload_id?: string
version?: string
}
Relationships: [
{
foreignKeyName: "s3_multipart_uploads_parts_bucket_id_fkey"
columns: ["bucket_id"]
isOneToOne: false
referencedRelation: "buckets"
referencedColumns: ["id"]
},
{
foreignKeyName: "s3_multipart_uploads_parts_upload_id_fkey"
columns: ["upload_id"]
isOneToOne: false
referencedRelation: "s3_multipart_uploads"
referencedColumns: ["id"]
},
]
}
}
Views: {
[_ in never]: never
}
Functions: {
can_insert_object: {
Args: {
bucketid: string
name: string
owner: string
metadata: Json
}
Returns: undefined
}
extension: {
Args: {
name: string
}
Returns: string
}
filename: {
Args: {
name: string
}
Returns: string
}
foldername: {
Args: {
name: string
}
Returns: string[]
}
get_size_by_bucket: {
Args: Record<PropertyKey, never>
Returns: {
size: number
bucket_id: string
}[]
}
list_multipart_uploads_with_delimiter: {
Args: {
bucket_id: string
prefix_param: string
delimiter_param: string
max_keys?: number
next_key_token?: string
next_upload_token?: string
}
Returns: {
key: string
id: string
created_at: string
}[]
}
list_objects_with_delimiter: {
Args: {
bucket_id: string
prefix_param: string
delimiter_param: string
max_keys?: number
start_after?: string
next_token?: string
}
Returns: {
name: string
id: string
metadata: Json
updated_at: string
}[]
}
operation: {
Args: Record<PropertyKey, never>
Returns: string
}
search: {
Args: {
prefix: string
bucketname: string
limits?: number
levels?: number
offsets?: number
search?: string
sortcolumn?: string
sortorder?: string
}
Returns: {
name: string
id: string
updated_at: string
created_at: string
last_accessed_at: string
metadata: Json
}[]
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
type PublicSchema = Database[Extract<keyof Database, "public">]
export type Tables<
PublicTableNameOrOptions extends
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
PublicSchema["Views"])
? (PublicSchema["Tables"] &
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
PublicEnumNameOrOptions extends
| keyof PublicSchema["Enums"]
| { schema: keyof Database },
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = PublicEnumNameOrOptions extends { schema: keyof Database }
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
? PublicSchema["Enums"][PublicEnumNameOrOptions]
: never