Signup flow + switch to serverSupabaseUser in context (fixes token refresh issue)+ lib upgrades for nuxt/supabase and nuxt/trpc
This commit is contained in:
12
README.md
12
README.md
@@ -31,6 +31,12 @@ pnpm install --shamefully-hoist
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the Stripe thingy
|
||||
|
||||
```bash
|
||||
stripe listen --forward-to localhost:3000/webhook
|
||||
```
|
||||
|
||||
Start the development server on http://localhost:3000
|
||||
|
||||
```bash
|
||||
@@ -77,7 +83,7 @@ npm run dev -- -o
|
||||
## Setup Supabase
|
||||
|
||||
To setup supabase and middleware, loosely follow instructions from https://www.youtube.com/watch?v=IcaL1RfnU44
|
||||
|
||||
remember to update email template
|
||||
Supabase - new account (free tier), used github oath for supabase account
|
||||
|
||||
```
|
||||
@@ -139,8 +145,8 @@ I set up a Stripe account with a couple of 'Products' with a single price each t
|
||||
-- add a pricing page....should be the default redirect from signup if the user has no active plan.. not sure whether to use a 'blank' plan or make plan nullable (basic pricing page is done - decided on 'no plan' plan)
|
||||
-- figure out what to do with Plan Name. Could add Plan Name to account record and copy over at time of account creation or updation. could pull from the Plan record for display.... but makes it difficult to change... should be loosely coupled, maybe use first approach (done)
|
||||
-- figure out when/how plan changes.. is it triggered by webhook? (Done, webhook looks up product info on plan record and updates plan info)
|
||||
-- Plan info is all over the place... product id is on the plan record in the db, pricing id's are on the pricing page template. would it be too crazy to have an admin page to administer pricing and plan/product info?
|
||||
-- What to do with pricing page? should probably change depending on current account information i.e. buttons say 'upgrade' for plans > current and maybe 'downgrade' for plans < current?
|
||||
-- Plan info is all over the place... product id is on the plan record in the db, pricing id's are on the pricing page template. would it be too crazy to have an admin page to administer pricing and plan/product info? (scratch, current system works ok)
|
||||
-- What to do with pricing page? should probably change depending on current account information i.e. buttons say 'upgrade' for plans > current and maybe 'downgrade' for plans < current? (Add an 'order' to the plan... basically an integer indicating how 'good' the plan is so that if your current account plan order (yes it's also copied onto the account), is lower than the plan on the pricing page, it says 'upgrade', otherwise 'downgrade'... on second thought, maybe just use plan name and if it's not == to your current plan, say 'switch to plan')
|
||||
# Admin Functions Scenario (shitty test)
|
||||
Pre-condition
|
||||
User 3 (encumbent id=3) - Owner of own single user account. Admin of Team account
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>Nuxt 3 Boilerplate - AppFooter</h3>
|
||||
<hr/>
|
||||
<span><NuxtLink to="/terms">Terms Of Service</NuxtLink></span>
|
||||
<span> | <NuxtLink to="/privacy">Privacy</NuxtLink></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3>Nuxt 3 Boilerplate - AppHeader</h3>
|
||||
<!-- logged in & sign out -->
|
||||
<div v-if="user">logged in as: {{ user.email }}: <button @click="signout()">Sign Out</button></div>
|
||||
<div v-if="!user">Not Logged in</div>
|
||||
<h3>Nuxt 3 Boilerplate - To the Moon!</h3>
|
||||
<div>
|
||||
<NuxtLink to="/">Boilerplate</NuxtLink>
|
||||
| <NuxtLink to="/dashboard">Dashboard</NuxtLink>
|
||||
| <NuxtLink to="/pricing">Pricing</NuxtLink>
|
||||
| <NuxtLink to="/account">Account</NuxtLink>
|
||||
<span v-if="!user"><NuxtLink to="/">Nuxt 3 Boilerplate</NuxtLink> | </span>
|
||||
<span><NuxtLink to="/pricing">Pricing</NuxtLink></span>
|
||||
<span v-if="user"> | <NuxtLink to="/dashboard">Dashboard</NuxtLink></span>
|
||||
<span v-if="user"> | <NuxtLink to="/account">Account</NuxtLink></span>
|
||||
<span v-if="!user"> | <NuxtLink to="/signin">Sign In</NuxtLink></span>
|
||||
<span v-if="user"> | <a href="#" @click.prevent="signout()">Sign out</a></span>
|
||||
<span v-if="user"> | logged in as: {{ user.email }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Account Switching -->
|
||||
|
||||
@@ -142,7 +142,7 @@ export default class UserAccountService {
|
||||
return prisma_client.membership.create({
|
||||
data: {
|
||||
user_id: user_id,
|
||||
account_id: account_id,
|
||||
account_id,
|
||||
access: ACCOUNT_ACCESS.READ_ONLY
|
||||
},
|
||||
include: {
|
||||
|
||||
@@ -12,4 +12,21 @@ export class UtilService {
|
||||
if (error instanceof Error) return error.message
|
||||
return String(error)
|
||||
}
|
||||
|
||||
public static circleSafeStringify(obj: any) {
|
||||
let cache: any[] = [];
|
||||
let str = JSON.stringify(obj, function(key, value) {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (cache.indexOf(value) !== -1) {
|
||||
// Circular reference found, discard key
|
||||
return;
|
||||
}
|
||||
// Store value in our collection
|
||||
cache.push(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
cache = []; // reset the cache
|
||||
return str;
|
||||
}
|
||||
}
|
||||
824
package-lock.json
generated
824
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/supabase": "^0.3.0",
|
||||
"@nuxtjs/supabase": "^0.3.1",
|
||||
"@prisma/client": "^4.9.0",
|
||||
"nuxt": "^3.1.1",
|
||||
"prisma": "^4.9.0"
|
||||
@@ -20,7 +20,7 @@
|
||||
"pinia": "^2.0.30",
|
||||
"stripe": "^11.12.0",
|
||||
"superjson": "^1.12.2",
|
||||
"trpc-nuxt": "^0.5.0",
|
||||
"trpc-nuxt": "^0.8.0",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const user = useSupabaseUser()
|
||||
const supabase = useSupabaseAuthClient();
|
||||
watchEffect(() => {
|
||||
if (user.value) {
|
||||
navigateTo('/dashboard', {replace: true})
|
||||
@@ -10,6 +9,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>Index</h3>
|
||||
<button @click="supabase.auth.signInWithOAuth({provider: 'google'})">Connect with Google</button>
|
||||
Nuxt 3 (SAAS) Boilerplate is very nice, why don't you <NuxtLink to="/signup">Get Started</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,25 +9,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>Pricing</h3>
|
||||
<p>Current Plan: {{ activeMembership?.account.plan_name }}</p>
|
||||
<div>
|
||||
<label for="submit">Free Trial (1 Month)</label>
|
||||
<NuxtLink to="/dashboard">Continue to Dashboard</NuxtLink>
|
||||
<ul>
|
||||
<li>10 Notes</li>
|
||||
<li>Single User</li>
|
||||
</ul>
|
||||
<NuxtLink v-if="activeMembership && (activeMembership?.account.plan_name === 'Free Trial')" to="/dashboard"> Continue to Dashboard</NuxtLink>
|
||||
</div>
|
||||
|
||||
<form action="/create-checkout-session" method="POST">
|
||||
<label for="submit">Individual Plan, Normal Price</label>
|
||||
<ul>
|
||||
<li>100 Notes</li>
|
||||
<li>Single User</li>
|
||||
</ul>
|
||||
|
||||
<input type="hidden" name="price_id" value="price_1MpOiwJfLn4RhYiLqfy6U8ZR" />
|
||||
<input type="hidden" name="account_id" :value="activeMembership?.account_id" />
|
||||
|
||||
<button type="submit" :disabled="!activeMembership || (activeMembership.access !== ACCOUNT_ACCESS.OWNER && activeMembership.access !== ACCOUNT_ACCESS.ADMIN)">Checkout</button>
|
||||
<span v-if="!activeMembership"> <NuxtLink to="/signup">Sign Up</NuxtLink> </span>
|
||||
<button type="submit" v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access !== ACCOUNT_ACCESS.ADMIN) && (activeMembership?.account.plan_name !== 'Individual Plan')">Checkout</button>
|
||||
</form>
|
||||
|
||||
<form action="/create-checkout-session" method="POST">
|
||||
<label for="submit">Team Plan, Normal Price</label>
|
||||
<ul>
|
||||
<li>200 Notes</li>
|
||||
<li>Up to 10 Team Members</li>
|
||||
</ul>
|
||||
|
||||
<input type="hidden" name="price_id" value="price_1MpOjtJfLn4RhYiLsjzAso90" />
|
||||
<input type="hidden" name="account_id" :value="activeMembership?.account_id" />
|
||||
|
||||
<button type="submit" :disabled="!activeMembership || (activeMembership.access !== ACCOUNT_ACCESS.OWNER && activeMembership.access !== ACCOUNT_ACCESS.ADMIN)">Checkout</button>
|
||||
<span v-if="!activeMembership"> <NuxtLink to="/signup">Sign Up</NuxtLink> </span>
|
||||
<button type="submit" v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN) && (activeMembership?.account.plan_name !== 'Team Plan')">Checkout</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
42
pages/signin.vue
Normal file
42
pages/signin.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
const user = useSupabaseUser()
|
||||
const supabase = useSupabaseAuthClient();
|
||||
|
||||
const loading = ref(false)
|
||||
const email = ref('')
|
||||
|
||||
const handleOtpLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
alert(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (user.value) {
|
||||
navigateTo('/dashboard', {replace: true})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<h3>Sign In</h3>
|
||||
<form @submit.prevent="handleOtpLogin">
|
||||
<label for="email">Email:</label>
|
||||
<input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" />
|
||||
<p>By signing in, I agree to the <NuxtLink to="/privacy">Privacy Statement</NuxtLink> and <NuxtLink to="/terms">Terms of Service</NuxtLink>.</p>
|
||||
|
||||
<button type="submit" :disabled="loading">Sign In using Magic Link</button>
|
||||
</form>
|
||||
|
||||
<p>or sign in with</p>
|
||||
|
||||
<button @click="supabase.auth.signInWithOAuth({provider: 'google'})">Google</button>
|
||||
</div>
|
||||
</template>
|
||||
42
pages/signup.vue
Normal file
42
pages/signup.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
const user = useSupabaseUser()
|
||||
const supabase = useSupabaseAuthClient();
|
||||
|
||||
const loading = ref(false)
|
||||
const email = ref('')
|
||||
|
||||
const handleOtpLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
alert(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (user.value) {
|
||||
navigateTo('/dashboard', {replace: true})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<h3>Sign Up</h3>
|
||||
<form @submit.prevent="handleOtpLogin">
|
||||
<label for="email">Email:</label>
|
||||
<input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" />
|
||||
<p>By proceeding, I agree to the <NuxtLink to="/privacy">Privacy Statement</NuxtLink> and <NuxtLink to="/terms">Terms of Service</NuxtLink>.</p>
|
||||
|
||||
<button type="submit" :disabled="loading">Sign Up using Magic Link</button>
|
||||
</form>
|
||||
|
||||
<p>or sign up with</p>
|
||||
|
||||
<button @click="supabase.auth.signInWithOAuth({provider: 'google'})">Google</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,6 +18,6 @@ try{
|
||||
<span v-if="!customer.deleted">We appreciate your business {{customer.name}}!</span>
|
||||
<span v-if="customer.deleted">It appears your stripe customer information has been deleted!</span>
|
||||
</p>
|
||||
<p><NuxtLink to="/dashboard">To Your Dashboard</NuxtLink></p>
|
||||
<p>Checkout our reasonable <NuxtLink to="/pricing">Pricing</NuxtLink></p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,36 +1,30 @@
|
||||
import { inferAsyncReturnType, TRPCError } from '@trpc/server'
|
||||
import { H3Event } from 'h3';
|
||||
import { serverSupabaseClient } from '#supabase/server';
|
||||
import SupabaseClient from '@supabase/supabase-js/dist/module/SupabaseClient';
|
||||
import { serverSupabaseUser } from '#supabase/server'
|
||||
import { User } from '@supabase/supabase-js';
|
||||
import UserAccountService, { FullDBUser } from '~~/lib/services/user.account.service';
|
||||
|
||||
let supabase: SupabaseClient | undefined
|
||||
|
||||
export async function createContext(event: H3Event){
|
||||
let user: User | null = null;
|
||||
let dbUser: FullDBUser | null = null;
|
||||
|
||||
if (!supabase) {
|
||||
supabase = serverSupabaseClient(event)
|
||||
}
|
||||
if (!user) {
|
||||
({data: { user }} = await supabase.auth.getUser());
|
||||
user = await serverSupabaseUser(event);
|
||||
}
|
||||
if (!dbUser && user) {
|
||||
const userService = new UserAccountService();
|
||||
dbUser = await userService.getFullUserBySupabaseId(user.id);
|
||||
|
||||
if (!dbUser && user) {
|
||||
dbUser = await userService.createUser( user.id, user.user_metadata.full_name, user.email?user.email:"no@email.supplied" );
|
||||
console.log(`\n Created user \n ${JSON.stringify(dbUser)}\n`);
|
||||
dbUser = await userService.createUser(user.id, user.user_metadata.full_name?user.user_metadata.full_name:"no name supplied", user.email?user.email:"no@email.supplied" );
|
||||
console.log(`\n Created DB User \n ${JSON.stringify(dbUser)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if(!user || !dbUser) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Unable to fetch user data, please try again later. Missing ->[supabase:${(!supabase)},user:${(!user)},dbUser:${(!dbUser)}]`,
|
||||
message: `Unable to fetch user data, please try again later. Missing ->[user:${(!user)},dbUser:${(!dbUser)}]`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user