sveltekit-pocketbase-starter
Advanced tools
Comparing version 0.0.1 to 0.1.0
{ | ||
"name": "sveltekit-pocketbase-starter", | ||
"version": "0.0.1", | ||
"version": "0.1.0", | ||
"author": "Dominick Caponi <d.caponi1@gmail.com>", | ||
@@ -27,4 +27,6 @@ "license": "MIT", | ||
"dependencies": { | ||
"pocketbase": "^0.15.2" | ||
"jsonwebtoken": "^9.0.2", | ||
"pocketbase": "^0.15.2", | ||
"stripe": "^14.11.0" | ||
} | ||
} |
@@ -6,3 +6,3 @@ # SvelteKit PocketBase Auth Starter Kit | ||
## Getting Started - Local Development | ||
1. Set `VITE_POCKETBASE_URL=http://127.0.0.1:5555` in `AccountsApp/.env.local` | ||
1. Set `VITE_POCKETBASE_URL=http://127.0.0.1:5555` in `sveltekit-pocketbase-starter/.env.local` | ||
2. Startup Option - Makefile | ||
@@ -18,4 +18,5 @@ 1. From the `pocketbase` directory, Run `make frontend-up` to bring up the skeleton app and navigate to `/login` | ||
7. Create routes in `/routes` following standard SvelteKit developer guidance | ||
1. If you want them to be *protected* i.e. user is logged in to see the page, add the folder to `const protectedRoutes = ['/protected']` in `AccountsApp/src/routes/+layout.server.ts` | ||
1. If you want them to be *protected* i.e. user is logged in to see the page, add the folder to `const protectedRoutes = ['/protected']` in `sveltekit-pocketbase-starter/src/routes/+layout.server.ts` | ||
2. You can also configure the page an un-authed user gets bounced to (in this example its `/login`) | ||
8. If you intend to do e-commerce stuff see the e-commerce section below. You'll need to set up stripe test stuff. If not, ignore and if you really hate it, nuke the `/routes/buy` folder and all the stuff about nonce in `routes/+layout.server.ts` | ||
@@ -34,3 +35,3 @@ ## Adding to Existing Project | ||
1. Go to "Mail Settings" and set the "Verification Template" Action URL to `{APP_URL}/confirm-verification/{TOKEN}` | ||
1. There's a `confirm-verification` folder in the AccountsApp SvelteKit project that handles flipping the "verified" flag when a user visits the Action URL from their email. | ||
1. There's a `confirm-verification` folder in the sveltekit-pocketbase-starter SvelteKit project that handles flipping the "verified" flag when a user visits the Action URL from their email. | ||
2. Check "Use SMTP mail server" | ||
@@ -52,3 +53,3 @@ 3. If you're cheap like me and don't want to pay for a SendGrid or Mail Gun you can: | ||
1. Set the Redirect URI to `http://localhost:5173/callback` | ||
1. there's a `callback` folder in AccountsApp SvelteKit project that handles the auth code response and requests an access token. This is why we set the "Application URL" earlier | ||
1. there's a `callback` folder in sveltekit-pocketbase-starter SvelteKit project that handles the auth code response and requests an access token. This is why we set the "Application URL" earlier | ||
2. This should result in a Client ID and Client Secret | ||
@@ -64,3 +65,3 @@ 2. In PocketBase go to the Auth Providers setting in the left menu | ||
## Going to Production - Pocketbase | ||
## 🚀 Going to Production - Pocketbase | ||
1. Deploy PocketBase to a server per PocketBase instructions. I build and push a docker image with PB on it to ECR and run it on a VM in the cloud (see docker-compose.yml for example) | ||
@@ -73,3 +74,3 @@ 2. Configure the production PocketBase instance in a similar fashion to local development. The only differences are you'll use your domain instead of localhost `mydomain.com/_` to get into the admin panel for PocketBase and `mydomain.com` for the Application name settings. | ||
## You Should Know | ||
## 🧠 You Should Know | ||
1. This uses cookie based JWTs and therefore cookies are only shared if you're using https, and only over http (not accessable via javascript). Therefore ensure your frontend and backend share domains. | ||
@@ -79,1 +80,23 @@ 2. You can set the cookie samesite setting to something other than lax if you don't want to use OAuth. Lax is required for OAuth since your app will be using cookies while talking to 3rd party services. | ||
4. The design philosophy behind this is to have decent auth working in a fast, repeatable fashion using the cheapest setup possible. This is in no way suggested for enterprise grade auth flows, but if you have less than 10k daily active users it _should_ be fine. If you have 10k daily active users, go get funding and make scale and enterprise shit someone else's problem 😉 | ||
## 💰 E-Commerce Stuff | ||
1. Go to stripe and set up an account (I wont even begin to tell you how complicated getting an LLC set up is... legalzoom?). | ||
* Anyway, stripe lets you create public and secret keys. You'll need a pair for test (usually starting with `sk_test_`) and a pair for production (usually starting with just `sk_`) | ||
* Now you need to fill out all the paperwork with your EIN from the IRS and add all your support emails and terms and stuff. Do that if you got it. Primarily you need an EIN or your social but that only works if you're doing business as a sole proprietor | ||
* Finally create some products and put their IDs in the `.env.local` (see `.env-example` for a hint) | ||
2. next create a long-ish string of random garbage to be your JWT signing secret (see `.env-example`) | ||
* We need this to sign nonces | ||
* The nonce is a random string of characters we'll include with the product details in a signed JWT. We'll stick one in the success callback url we give to stripe, so when they call us back after successful payment we'll know | ||
* A) the payment went through | ||
* B) who paid | ||
* C) that nobody intercepted the payment | ||
* D) nobody tries to modify the package after paying (kinda like the nice people checking reciepts at the costco exit making sure you didn't pull a switcheroo with the boxes) | ||
* We associate the nonce to the user as well by saving that in a field in pocketbase | ||
* speaking of which, make sure you add a `nonce` column to the users table in pocketbase | ||
3. modify `/routes/buy` to the endpoint what your offerings page is called | ||
4. update `offerings` in `/routes/buy/+page.server.ts` to reflect what you're selling. Give price, label and sku. I left my old stuff in there as an example. | ||
5. update the `urls` in `/routes/buy/+page.server.ts` to reflect your prod domains and whatnot. (actually this should be env vars but I'm lazy) | ||
6. Make `/routes/buy/+page.svelte` pretty. Offerings should come out from server so just decorate this page, no need for anything fancy. | ||
7. Fire it up and test. Stripe offers a few test payment card numbers that will let you test the flow. Check that: | ||
* when you hit the stripe page pocketbase shows the user having a new nonce. You can decode it on jwt.io to make sure its the right selection | ||
* when you come back from stripe, you have a nonce query string and that it matches what _was_ in pocketbase. pocketbase should show no nonce now. |
import type { Handle } from '@sveltejs/kit'; | ||
import PocketBase from 'pocketbase'; | ||
import type { RequestEvent, RequestHandler } from './$types'; | ||
import { VITE_POCKETBASE_URL } from '$env/static/private'; | ||
@@ -30,2 +31,2 @@ | ||
return response; | ||
}; | ||
}; |
import { redirect } from '@sveltejs/kit'; | ||
import { decodeJwt, validateJwt } from '$lib/jwt'; | ||
import type { LayoutServerLoad } from './$types'; | ||
import { VITE_NONCE_SIGNING_SECRET } from '$env/static/private'; | ||
@@ -10,2 +12,34 @@ export const load: LayoutServerLoad = async ({ locals, url }) => { | ||
} | ||
const currentUserToken = decodeJwt(locals.pb?.authStore.token || ''); | ||
if (currentUserToken){ | ||
let currentUser = await locals.pb?.collection('users').getOne(currentUserToken.id); | ||
if (currentUser){ | ||
// if theres an unresolved nonce/purchase... | ||
if (currentUser.nonce) { | ||
// go get the nonce token created in routes/buy/+page.server.ts | ||
const nonceToken = url.searchParams.get('nonce'); | ||
if (nonceToken) { | ||
// validate that its not tampered with | ||
if (validateJwt(nonceToken, VITE_NONCE_SIGNING_SECRET)){ | ||
// decode the nonce tokens | ||
const userToken = decodeJwt(currentUser.nonce) | ||
const { nonce } = decodeJwt(nonceToken) | ||
// make sure the random nonce values match | ||
if (userToken.nonce === nonce) { | ||
// credit the account | ||
await locals.pb?.collection('users').update(currentUserToken.id, { nonce: '', credits: (currentUser.credits + userToken.credits)}); | ||
currentUser = await locals.pb?.collection('users').getOne(currentUserToken.id); | ||
// todo handle failed updates (maybe cross check with stripe periodically) | ||
} | ||
} | ||
} | ||
// todo handle stuck nonce (notify owner, put nonce in state or something) | ||
} | ||
return { | ||
loggedIn: locals.pb?.authStore.isValid, | ||
} | ||
} | ||
} | ||
}; | ||
@@ -1,2 +0,2 @@ | ||
import { fail, redirect, type Cookies } from '@sveltejs/kit'; | ||
import { fail, redirect, type Cookies, type HttpError } from '@sveltejs/kit'; | ||
import type { PageServerLoad } from './$types'; | ||
@@ -38,4 +38,2 @@ | ||
} | ||
}; | ||
@@ -50,4 +48,11 @@ | ||
await locals.pb?.collection('users').create({email, password, passwordConfirm}) | ||
locals.pb?.collection('users').requestVerification(email) | ||
if (password !== passwordConfirm) { | ||
return fail(422, { email, error: true, message: "password and password confirm must match" }); | ||
} | ||
try { | ||
await locals.pb?.collection('users').create({email, password, passwordConfirm}) | ||
locals.pb?.collection('users').requestVerification(email) | ||
} catch (e: any) { | ||
return fail(422, {error: true, message: e.response.data.email.message}) | ||
} | ||
@@ -78,8 +83,8 @@ return loginWithEmailPassword(locals, cookies, email, password) | ||
if(e.status >= 400 && e.status <= 500){ | ||
return fail(e.status, { email, authFail: true }); | ||
return fail(e.status, { email, error: true, message: "failed to authenticate" }); | ||
} | ||
if (e.status >=500){ | ||
return fail(e.status, { email, authDown: true }); | ||
return fail(e.status, { email, error: true, message: "authentication server could not be reached" }); | ||
} | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
366618
29
350
95
3
+ Addedjsonwebtoken@^9.0.2
+ Addedstripe@^14.11.0
+ Added@types/node@22.13.4(transitive)
+ Addedbuffer-equal-constant-time@1.0.1(transitive)
+ Addedcall-bind-apply-helpers@1.0.2(transitive)
+ Addedcall-bound@1.0.3(transitive)
+ Addeddunder-proto@1.0.1(transitive)
+ Addedecdsa-sig-formatter@1.0.11(transitive)
+ Addedes-define-property@1.0.1(transitive)
+ Addedes-errors@1.3.0(transitive)
+ Addedes-object-atoms@1.1.1(transitive)
+ Addedfunction-bind@1.1.2(transitive)
+ Addedget-intrinsic@1.2.7(transitive)
+ Addedget-proto@1.0.1(transitive)
+ Addedgopd@1.2.0(transitive)
+ Addedhas-symbols@1.1.0(transitive)
+ Addedhasown@2.0.2(transitive)
+ Addedjsonwebtoken@9.0.2(transitive)
+ Addedjwa@1.4.1(transitive)
+ Addedjws@3.2.2(transitive)
+ Addedlodash.includes@4.3.0(transitive)
+ Addedlodash.isboolean@3.0.3(transitive)
+ Addedlodash.isinteger@4.0.4(transitive)
+ Addedlodash.isnumber@3.0.3(transitive)
+ Addedlodash.isplainobject@4.0.6(transitive)
+ Addedlodash.isstring@4.0.1(transitive)
+ Addedlodash.once@4.1.1(transitive)
+ Addedmath-intrinsics@1.1.0(transitive)
+ Addedms@2.1.3(transitive)
+ Addedobject-inspect@1.13.4(transitive)
+ Addedqs@6.14.0(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsemver@7.7.1(transitive)
+ Addedside-channel@1.1.0(transitive)
+ Addedside-channel-list@1.0.0(transitive)
+ Addedside-channel-map@1.0.1(transitive)
+ Addedside-channel-weakmap@1.0.2(transitive)
+ Addedstripe@14.25.0(transitive)
+ Addedundici-types@6.20.0(transitive)