mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-12 18:43:50 +00:00
Merge pull request #24 from jcreek/14-add-ability-to-record-that-a-user-has-gained-access-to-a-software-product-after-purchase-1
14 add ability to record that a user has gained access to a software product after purchase
This commit is contained in:
@@ -41,4 +41,10 @@ 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)
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
Generated
+433
-75
@@ -8,8 +8,8 @@
|
||||
"name": "SvelteKitSaasBoilerplate",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@supabase/supabase-js": "^2.42.0",
|
||||
"@supabase/ssr": "^0.4.0",
|
||||
"@supabase/supabase-js": "^2.44.2",
|
||||
"stripe": "^15.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -33,6 +33,7 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"supabase": "^1.178.2",
|
||||
"svelte": "^4.2.8",
|
||||
"svelte-check": "^3.6.2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
@@ -2578,6 +2579,19 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
@@ -2953,7 +2967,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -3012,17 +3025,19 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"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==",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz",
|
||||
"integrity": "sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
@@ -3031,6 +3046,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -3039,17 +3055,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz",
|
||||
"integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.4.tgz",
|
||||
"integrity": "sha512-wdq+2hZpgw0r2ldRs87d3U08Y8BrsO1bZxPNqbImpYshAEkusDz4vufR8KaqujKxqewmXS6YnUhuRVdvSEIKCA==",
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.2.tgz",
|
||||
"integrity": "sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14",
|
||||
"@types/phoenix": "^1.5.4",
|
||||
@@ -3058,36 +3076,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/ssr": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.1.0.tgz",
|
||||
"integrity": "sha512-bIVrkqjAK5G3KjkIMKYKtAOlCgRRplEWjrlyRyXSOYtgDieiOhk2ZyNAPsEOa1By9OZVxuX5eAW1fitdnuxayw==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.4.0.tgz",
|
||||
"integrity": "sha512-6WS3NUvHDhCPAFN2kJ79AQDO8+M9fJ7y2fYpxgZqIuJEpnnGsHDNnB5Xnv8CiaJIuRU+0pKboy62RVZBMfZ0Lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^0.5.0",
|
||||
"ramda": "^0.29.0"
|
||||
"cookie": "^0.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.9.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.33.1"
|
||||
"@supabase/supabase-js": "^2.43.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz",
|
||||
"integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.42.7",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.42.7.tgz",
|
||||
"integrity": "sha512-BEIEYe5KJpzd8Z3k4CKyjNuBmgSihDdE8MJO/Fg7O5h/lQg8qOp1MMWLWPP5aVKt4TYled/W82ePNJflqc2JbQ==",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.64.1",
|
||||
"@supabase/functions-js": "2.3.1",
|
||||
"@supabase/auth-js": "2.64.2",
|
||||
"@supabase/functions-js": "2.4.1",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "1.15.2",
|
||||
"@supabase/realtime-js": "2.9.4",
|
||||
"@supabase/storage-js": "2.5.5"
|
||||
"@supabase/postgrest-js": "1.15.7",
|
||||
"@supabase/realtime-js": "2.10.2",
|
||||
"@supabase/storage-js": "2.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@surma/rollup-plugin-off-main-thread": {
|
||||
@@ -3195,15 +3218,6 @@
|
||||
"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",
|
||||
@@ -3280,9 +3294,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz",
|
||||
"integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA=="
|
||||
"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"
|
||||
},
|
||||
"node_modules/@types/pug": {
|
||||
"version": "2.0.10",
|
||||
@@ -3313,6 +3328,7 @@
|
||||
"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": "*"
|
||||
}
|
||||
@@ -3582,6 +3598,19 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.1",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -3920,6 +3949,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/bin-links": {
|
||||
"version": "4.0.4",
|
||||
"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",
|
||||
"read-cmd-shim": "^4.0.0",
|
||||
"write-file-atomic": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -3953,12 +3998,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -4171,6 +4217,16 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cmd-shim": {
|
||||
"version": "6.0.3",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/code-red": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
|
||||
@@ -4288,9 +4344,10 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -4396,6 +4453,16 @@
|
||||
"url": "https://opencollective.com/daisyui"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
|
||||
@@ -5190,6 +5257,30 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@@ -5226,10 +5317,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -5299,6 +5391,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@@ -5658,6 +5763,20 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ico-endec": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/ico-endec/-/ico-endec-0.1.6.tgz",
|
||||
@@ -5963,6 +6082,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -6515,14 +6635,91 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
|
||||
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.0.1",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"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"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"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"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
@@ -6624,6 +6821,45 @@
|
||||
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"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",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
@@ -6648,6 +6884,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-normalize-package-bin": {
|
||||
"version": "3.0.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -6759,6 +7005,13 @@
|
||||
"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",
|
||||
@@ -6805,16 +7058,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
|
||||
"integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
|
||||
"version": "1.11.1",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@@ -7246,15 +7500,6 @@
|
||||
"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",
|
||||
@@ -7298,6 +7543,16 @@
|
||||
"pify": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cmd-shim": {
|
||||
"version": "4.0.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
@@ -8339,6 +8594,26 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/supabase": {
|
||||
"version": "1.178.2",
|
||||
"resolved": "https://registry.npmjs.org/supabase/-/supabase-1.178.2.tgz",
|
||||
"integrity": "sha512-JdjNY56cF5PbuhFdhgdYah5qXOPRsLQn0kXaY7uapTFopdIFX4tRiFXqKSig8S0SdznF/y/f6vLdq30Sek1ZZQ==",
|
||||
"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"
|
||||
},
|
||||
"bin": {
|
||||
"supabase": "bin/supabase"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -8625,6 +8900,24 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz",
|
||||
"integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.0.1",
|
||||
"mkdirp": "^3.0.1",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
|
||||
@@ -8650,6 +8943,42 @@
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/mkdirp": {
|
||||
"version": "3.0.1",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/temp-dir": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||
@@ -8776,6 +9105,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -8795,7 +9125,8 @@
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.3.0",
|
||||
@@ -9206,15 +9537,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"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"
|
||||
@@ -9784,10 +10127,25 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
"version": "5.0.1",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
||||
+3
-2
@@ -33,6 +33,7 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"supabase": "^1.178.2",
|
||||
"svelte": "^4.2.8",
|
||||
"svelte-check": "^3.6.2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
@@ -41,8 +42,8 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@supabase/supabase-js": "^2.42.0",
|
||||
"@supabase/ssr": "^0.4.0",
|
||||
"@supabase/supabase-js": "^2.44.2",
|
||||
"stripe": "^15.4.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
Vendored
+6
-5
@@ -1,22 +1,23 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js'
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js';
|
||||
import type { Database } from '$types/supabase';
|
||||
declare global {
|
||||
const __DATE__: string;
|
||||
const __RELOAD_SW__: boolean;
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
supabase: SupabaseClient
|
||||
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 {}
|
||||
}
|
||||
|
||||
+20
-17
@@ -5,45 +5,48 @@ 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: {
|
||||
get: (key) => event.cookies.get(key),
|
||||
getAll: () => event.cookies.getAll(),
|
||||
/**
|
||||
* 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)
|
||||
* SvelteKit's cookies API requires `path` to be explicitly set in
|
||||
* the cookie options. Setting `path` to `/` replicates previous/
|
||||
* standard behavior.
|
||||
*/
|
||||
set: (key, value, options) => {
|
||||
event.cookies.set(key, value, { ...options, path: '/' });
|
||||
},
|
||||
remove: (key, options) => {
|
||||
event.cookies.delete(key, { ...options, path: '/' });
|
||||
setAll: (cookiesToSet) => {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
event.cookies.set(name, value, { ...options, path: '/' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
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';
|
||||
return name === 'content-range' || name === 'x-supabase-api-version';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<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({
|
||||
@@ -12,7 +17,7 @@
|
||||
password: password,
|
||||
options: {
|
||||
// Redirect URL after successful sign-up
|
||||
redirectTo: '/welcome'
|
||||
redirectTo: '/account'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,6 +25,21 @@
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
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
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
||||
const { session, user } = await safeGetSession();
|
||||
|
||||
return {
|
||||
session,
|
||||
user
|
||||
user,
|
||||
cookies: cookies.getAll()
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
let cookiesAccepted = false;
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
|
||||
if (_session?.expires_at !== session?.expires_at) {
|
||||
const { data } = supabase.auth.onAuthStateChange((event, newSession) => {
|
||||
if (newSession?.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="/profile"> Profile </a>
|
||||
<a href="/account"> Account </a>
|
||||
</li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li><SignOut {supabase} /></li>
|
||||
|
||||
+16
-15
@@ -1,25 +1,26 @@
|
||||
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 = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch
|
||||
},
|
||||
cookies: {
|
||||
get(key) {
|
||||
if (!isBrowser()) {
|
||||
return JSON.stringify(data.session);
|
||||
const supabase = isBrowser()
|
||||
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch
|
||||
}
|
||||
|
||||
const cookie = parse(document.cookie);
|
||||
return cookie[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch
|
||||
},
|
||||
cookies: {
|
||||
getAll() {
|
||||
return data.cookies;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { type User } from '@supabase/auth-js';
|
||||
import SignUp from '$lib/components/SignUp.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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, '/');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
<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>
|
||||
@@ -0,0 +1,95 @@
|
||||
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}`
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<p>Login error</p>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -0,0 +1,4 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
||||
@@ -0,0 +1,194 @@
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "SvelteKitSaasBoilerplate"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. `public` is always included.
|
||||
schemas = ["public", "graphql_public"]
|
||||
# Extra schemas to add to the search_path of every request. `public` is always included.
|
||||
extra_search_path = ["public", "extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
# 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
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
# How many server connections to allow per user/database pair.
|
||||
default_pool_size = 20
|
||||
# Maximum number of client connections allowed.
|
||||
max_client_conn = 100
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||
# ip_version = "IPv6"
|
||||
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||
# max_header_length = 4096
|
||||
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
# 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.
|
||||
openai_api_key = "env(OPENAI_API_KEY)"
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
|
||||
[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"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://127.0.0.1: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.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
# Requires 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
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = 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]
|
||||
# subject = "You have been invited"
|
||||
# content_path = "./supabase/templates/invite.html"
|
||||
|
||||
[auth.sms]
|
||||
# Allow/disallow new user signups via SMS to your project.
|
||||
enable_signup = true
|
||||
# If enabled, users need to confirm their phone number before signing in.
|
||||
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]
|
||||
# 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]
|
||||
# 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
|
||||
account_sid = ""
|
||||
message_service_sid = ""
|
||||
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
[auth.external.apple]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
||||
# Overrides the default auth redirectUrl.
|
||||
redirect_uri = ""
|
||||
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||
# or any other third-party OIDC providers.
|
||||
url = ""
|
||||
# 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
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
|
||||
# Experimental features may be deprecated any time
|
||||
[experimental]
|
||||
# Configures Postgres storage engine to use OrioleDB (S3)
|
||||
orioledb_version = ""
|
||||
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
||||
s3_host = "env(S3_HOST)"
|
||||
# Configures S3 bucket region, eg. us-east-1
|
||||
s3_region = "env(S3_REGION)"
|
||||
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
||||
s3_access_key = "env(S3_ACCESS_KEY)"
|
||||
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
||||
s3_secret_key = "env(S3_SECRET_KEY)"
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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;
|
||||
Vendored
+653
@@ -0,0 +1,653 @@
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user