finally put #3 to bed with state. also introduce email/password signup and login
This commit is contained in:
@@ -12,6 +12,9 @@
|
||||
|
||||
async function signout() {
|
||||
await supabase.auth.signOut();
|
||||
if(accountStore){
|
||||
accountStore.signout();
|
||||
}
|
||||
navigateTo('/', {replace: true});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
onMounted(async () => {
|
||||
await accountStore.init();
|
||||
await accountStore.getActiveAccountMembers();
|
||||
});
|
||||
|
||||
function formatDate(date: Date | undefined){
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ACCOUNT_ACCESS } from '@prisma/client';
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { activeMembership } = storeToRefs(authStore);
|
||||
const accountStore = useAccountStore()
|
||||
const { activeMembership } = storeToRefs(accountStore);
|
||||
|
||||
onMounted(async () => {
|
||||
await authStore.initUser();
|
||||
await accountStore.init();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
const user = useSupabaseUser()
|
||||
const supabase = useSupabaseAuthClient();
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
|
||||
const handleOtpLogin = async () => {
|
||||
try {
|
||||
@@ -18,8 +21,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
const handleStandardLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { error } = await supabase.auth.signInWithPassword({ email: email.value, password: password.value })
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(async () => {
|
||||
if (user.value) {
|
||||
await accountStore.init();
|
||||
navigateTo('/dashboard', {replace: true})
|
||||
}
|
||||
})
|
||||
@@ -27,6 +43,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>Sign In</h3>
|
||||
<form @submit.prevent="handleStandardLogin">
|
||||
<label for="email">Email:</label>
|
||||
<input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" />
|
||||
<label for="password">Password:</label>
|
||||
<input class="inputField" type="password" id="password" placeholder="Password" v-model="password" />
|
||||
<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</button>
|
||||
</form>
|
||||
|
||||
<form @submit.prevent="handleOtpLogin">
|
||||
<label for="email">Email:</label>
|
||||
<input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" />
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
const handleOtpLogin = async () => {
|
||||
try {
|
||||
@@ -18,6 +20,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleStandardSignup = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data, error } = await supabase.auth.signUp({ email: email.value, password: password.value })
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (user.value) {
|
||||
navigateTo('/dashboard', {replace: true})
|
||||
@@ -27,6 +41,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>Sign Up</h3>
|
||||
<form @submit.prevent="handleStandardSignup">
|
||||
<label for="email">Email:</label>
|
||||
<input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" />
|
||||
<label for="password">Password:</label>
|
||||
<input class="inputField" type="password" id="password" placeholder="Password" v-model="password" />
|
||||
<label for="confirm_password">Confirm Password:</label>
|
||||
<input class="inputField" type="password" id="convirm_password" placeholder="Confirm Password" v-model="confirmPassword" />
|
||||
<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 || (confirmPassword !== password)">Sign Up</button>
|
||||
</form>
|
||||
|
||||
<form @submit.prevent="handleOtpLogin">
|
||||
<label for="email">Email:</label>
|
||||
<input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" />
|
||||
|
||||
@@ -8,19 +8,19 @@ import { MembershipWithAccount } from '~~/lib/services/service.types';
|
||||
Note on proliferation of Bang syntax... adminProcedure throws if either the ctx.dbUser or the ctx.activeAccountId is not available but the compiler can't figure that out so bang quiesces the null warning
|
||||
*/
|
||||
export const accountRouter = router({
|
||||
getDBUser: protectedProcedure
|
||||
getDBUser: publicProcedure
|
||||
.query(({ ctx }) => {
|
||||
return {
|
||||
dbUser: ctx.dbUser,
|
||||
}
|
||||
}),
|
||||
getActiveAccountId: protectedProcedure
|
||||
getActiveAccountId: publicProcedure
|
||||
.query(({ ctx }) => {
|
||||
return {
|
||||
activeAccountId: ctx.activeAccountId,
|
||||
}
|
||||
}),
|
||||
changeActiveAccount: adminProcedure
|
||||
changeActiveAccount: protectedProcedure
|
||||
.input(z.object({ account_id: z.number() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
ctx.activeAccountId = input.account_id;
|
||||
|
||||
@@ -36,7 +36,7 @@ const isAdminForInputAccountId = t.middleware(({ next, rawInput, ctx }) => {
|
||||
}
|
||||
const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId);
|
||||
if(!activeMembership || (activeMembership?.access !== ACCOUNT_ACCESS.ADMIN && activeMembership?.access !== ACCOUNT_ACCESS.OWNER)) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message:`activeMembership ${activeMembership?.id} is only ${activeMembership?.access}` });
|
||||
}
|
||||
|
||||
return next({ ctx });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ACCOUNT_ACCESS } from ".prisma/client"
|
||||
import { ACCOUNT_ACCESS } from '@prisma/client';
|
||||
import { defineStore } from "pinia"
|
||||
import { FullDBUser, MembershipWithUser } from "~~/lib/services/service.types";
|
||||
|
||||
@@ -54,21 +54,26 @@ export const useAccountStore = defineStore('account', {
|
||||
this.activeAccountId = activeAccountId;
|
||||
}
|
||||
}
|
||||
if(this.activeAccountMembers.length == 0){
|
||||
await this.getActiveAccountMembers();
|
||||
}
|
||||
},
|
||||
signout(){
|
||||
this.dbUser = null;
|
||||
this.activeAccountId = null;
|
||||
this.activeAccountMembers = [];
|
||||
},
|
||||
async getActiveAccountMembers(){
|
||||
const { $client } = useNuxtApp();
|
||||
const { data: memberships } = await $client.account.getAccountMembers.useQuery();
|
||||
if(memberships.value?.memberships){
|
||||
this.activeAccountMembers = memberships.value?.memberships;
|
||||
if(this.activeMembership && (this.activeMembership.access === ACCOUNT_ACCESS.ADMIN || this.activeMembership.access === ACCOUNT_ACCESS.OWNER)){
|
||||
const { $client } = useNuxtApp();
|
||||
const { data: memberships } = await $client.account.getAccountMembers.useQuery();
|
||||
if(memberships.value?.memberships){
|
||||
this.activeAccountMembers = memberships.value?.memberships;
|
||||
}
|
||||
}
|
||||
},
|
||||
async changeActiveAccount(account_id: number){
|
||||
const { $client } = useNuxtApp();
|
||||
this.activeAccountId = account_id;
|
||||
await $client.account.changeActiveAccount.mutate({account_id}); // sets active account on context for other routers and sets the preference in a cookie
|
||||
|
||||
this.activeAccountId = account_id; // because this is used as a trigger to some other components, NEEDS TO BE AFTER THE MUTATE CALL
|
||||
await this.getActiveAccountMembers(); // these relate to the active account and need to ber re-fetched
|
||||
},
|
||||
async changeAccountName(new_name: string){
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
import { Note } from ".prisma/client"
|
||||
import { defineStore } from "pinia"
|
||||
import { defineStore, storeToRefs } from "pinia"
|
||||
import { Ref } from "vue";
|
||||
|
||||
interface State {
|
||||
notes: Note[]
|
||||
}
|
||||
|
||||
export const useNotesStore = defineStore('notes', {
|
||||
state: (): State => {
|
||||
return {
|
||||
notes: [],
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async fetchNotesForCurrentUser() {
|
||||
const { $client } = useNuxtApp();
|
||||
const { notes } = await $client.notes.getForCurrentUser.query();
|
||||
if(notes){
|
||||
this.notes = notes;
|
||||
}
|
||||
},
|
||||
/*
|
||||
Note) the Notes Store needs to be a 'Setup Store' (https://pinia.vuejs.org/core-concepts/#setup-stores)
|
||||
because this enables the use of the watch on the Account Store
|
||||
If the UI does not need to dynamically respond to a change in the active Account e.g. if state is always retrieved with an explicit fetch after onMounted.
|
||||
then an Options store can be used.
|
||||
*/
|
||||
export const useNotesStore = defineStore('notes', () => {
|
||||
const accountStore = useAccountStore()
|
||||
const { activeAccountId } = storeToRefs(accountStore);
|
||||
|
||||
let _notes: Ref<Note[]> = ref([]);
|
||||
|
||||
async function fetchNotesForCurrentUser() {
|
||||
const { $client } = useNuxtApp();
|
||||
const { notes } = await $client.notes.getForCurrentUser.query();
|
||||
if(notes){
|
||||
_notes.value = notes;
|
||||
}
|
||||
}
|
||||
|
||||
// if the active account changes, fetch notes again (i.e dynamic.. probabl overkill)
|
||||
watch(activeAccountId, async (val, oldVal)=> {
|
||||
await fetchNotesForCurrentUser()
|
||||
});
|
||||
|
||||
return { notes: _notes, fetchNotesForCurrentUser }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user