finally put #3 to bed with state. also introduce email/password signup and login

This commit is contained in:
Michael Dausmann
2023-04-13 20:11:18 +10:00
parent 028a7dda45
commit 6a7e7ec9ac
9 changed files with 105 additions and 32 deletions

View File

@@ -12,6 +12,9 @@
async function signout() { async function signout() {
await supabase.auth.signOut(); await supabase.auth.signOut();
if(accountStore){
accountStore.signout();
}
navigateTo('/', {replace: true}); navigateTo('/', {replace: true});
} }
</script> </script>

View File

@@ -10,6 +10,7 @@
onMounted(async () => { onMounted(async () => {
await accountStore.init(); await accountStore.init();
await accountStore.getActiveAccountMembers();
}); });
function formatDate(date: Date | undefined){ function formatDate(date: Date | undefined){

View File

@@ -2,11 +2,11 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { ACCOUNT_ACCESS } from '@prisma/client'; import { ACCOUNT_ACCESS } from '@prisma/client';
const authStore = useAuthStore() const accountStore = useAccountStore()
const { activeMembership } = storeToRefs(authStore); const { activeMembership } = storeToRefs(accountStore);
onMounted(async () => { onMounted(async () => {
await authStore.initUser(); await accountStore.init();
}); });
</script> </script>
<template> <template>

View File

@@ -2,8 +2,11 @@
const user = useSupabaseUser() const user = useSupabaseUser()
const supabase = useSupabaseAuthClient(); const supabase = useSupabaseAuthClient();
const accountStore = useAccountStore()
const loading = ref(false) const loading = ref(false)
const email = ref('') const email = ref('')
const password = ref('')
const handleOtpLogin = async () => { const handleOtpLogin = async () => {
try { 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) { if (user.value) {
await accountStore.init();
navigateTo('/dashboard', {replace: true}) navigateTo('/dashboard', {replace: true})
} }
}) })
@@ -27,6 +43,16 @@
<template> <template>
<div> <div>
<h3>Sign In</h3> <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"> <form @submit.prevent="handleOtpLogin">
<label for="email">Email:</label> <label for="email">Email:</label>
<input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" /> <input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" />

View File

@@ -4,6 +4,8 @@
const loading = ref(false) const loading = ref(false)
const email = ref('') const email = ref('')
const password = ref('')
const confirmPassword = ref('')
const handleOtpLogin = async () => { const handleOtpLogin = async () => {
try { 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(() => { watchEffect(() => {
if (user.value) { if (user.value) {
navigateTo('/dashboard', {replace: true}) navigateTo('/dashboard', {replace: true})
@@ -27,6 +41,18 @@
<template> <template>
<div> <div>
<h3>Sign Up</h3> <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"> <form @submit.prevent="handleOtpLogin">
<label for="email">Email:</label> <label for="email">Email:</label>
<input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" /> <input class="inputField" type="email" id="email" placeholder="Your email" v-model="email" />

View File

@@ -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 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({ export const accountRouter = router({
getDBUser: protectedProcedure getDBUser: publicProcedure
.query(({ ctx }) => { .query(({ ctx }) => {
return { return {
dbUser: ctx.dbUser, dbUser: ctx.dbUser,
} }
}), }),
getActiveAccountId: protectedProcedure getActiveAccountId: publicProcedure
.query(({ ctx }) => { .query(({ ctx }) => {
return { return {
activeAccountId: ctx.activeAccountId, activeAccountId: ctx.activeAccountId,
} }
}), }),
changeActiveAccount: adminProcedure changeActiveAccount: protectedProcedure
.input(z.object({ account_id: z.number() })) .input(z.object({ account_id: z.number() }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
ctx.activeAccountId = input.account_id; ctx.activeAccountId = input.account_id;

View File

@@ -36,7 +36,7 @@ const isAdminForInputAccountId = t.middleware(({ next, rawInput, ctx }) => {
} }
const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId); const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId);
if(!activeMembership || (activeMembership?.access !== ACCOUNT_ACCESS.ADMIN && activeMembership?.access !== ACCOUNT_ACCESS.OWNER)) { 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 }); return next({ ctx });

View File

@@ -1,4 +1,4 @@
import { ACCOUNT_ACCESS } from ".prisma/client" import { ACCOUNT_ACCESS } from '@prisma/client';
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { FullDBUser, MembershipWithUser } from "~~/lib/services/service.types"; import { FullDBUser, MembershipWithUser } from "~~/lib/services/service.types";
@@ -54,21 +54,26 @@ export const useAccountStore = defineStore('account', {
this.activeAccountId = activeAccountId; this.activeAccountId = activeAccountId;
} }
} }
if(this.activeAccountMembers.length == 0){ },
await this.getActiveAccountMembers(); signout(){
} this.dbUser = null;
this.activeAccountId = null;
this.activeAccountMembers = [];
}, },
async getActiveAccountMembers(){ async getActiveAccountMembers(){
const { $client } = useNuxtApp(); if(this.activeMembership && (this.activeMembership.access === ACCOUNT_ACCESS.ADMIN || this.activeMembership.access === ACCOUNT_ACCESS.OWNER)){
const { data: memberships } = await $client.account.getAccountMembers.useQuery(); const { $client } = useNuxtApp();
if(memberships.value?.memberships){ const { data: memberships } = await $client.account.getAccountMembers.useQuery();
this.activeAccountMembers = memberships.value?.memberships; if(memberships.value?.memberships){
this.activeAccountMembers = memberships.value?.memberships;
}
} }
}, },
async changeActiveAccount(account_id: number){ async changeActiveAccount(account_id: number){
const { $client } = useNuxtApp(); 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 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 await this.getActiveAccountMembers(); // these relate to the active account and need to ber re-fetched
}, },
async changeAccountName(new_name: string){ async changeAccountName(new_name: string){

View File

@@ -1,23 +1,35 @@
import { Note } from ".prisma/client" import { Note } from ".prisma/client"
import { defineStore } from "pinia" import { defineStore, storeToRefs } from "pinia"
import { Ref } from "vue";
interface State { interface State {
notes: Note[] notes: Note[]
} }
export const useNotesStore = defineStore('notes', { /*
state: (): State => { Note) the Notes Store needs to be a 'Setup Store' (https://pinia.vuejs.org/core-concepts/#setup-stores)
return { because this enables the use of the watch on the Account Store
notes: [], 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.
}, */
actions: { export const useNotesStore = defineStore('notes', () => {
async fetchNotesForCurrentUser() { const accountStore = useAccountStore()
const { $client } = useNuxtApp(); const { activeAccountId } = storeToRefs(accountStore);
const { notes } = await $client.notes.getForCurrentUser.query();
if(notes){ let _notes: Ref<Note[]> = ref([]);
this.notes = notes;
} 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 }
}); });