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() {
|
async function signout() {
|
||||||
await supabase.auth.signOut();
|
await supabase.auth.signOut();
|
||||||
|
if(accountStore){
|
||||||
|
accountStore.signout();
|
||||||
|
}
|
||||||
navigateTo('/', {replace: true});
|
navigateTo('/', {replace: true});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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){
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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(){
|
||||||
|
if(this.activeMembership && (this.activeMembership.access === ACCOUNT_ACCESS.ADMIN || this.activeMembership.access === ACCOUNT_ACCESS.OWNER)){
|
||||||
const { $client } = useNuxtApp();
|
const { $client } = useNuxtApp();
|
||||||
const { data: memberships } = await $client.account.getAccountMembers.useQuery();
|
const { data: memberships } = await $client.account.getAccountMembers.useQuery();
|
||||||
if(memberships.value?.memberships){
|
if(memberships.value?.memberships){
|
||||||
this.activeAccountMembers = 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){
|
||||||
|
|||||||
@@ -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 { activeAccountId } = storeToRefs(accountStore);
|
||||||
|
|
||||||
|
let _notes: Ref<Note[]> = ref([]);
|
||||||
|
|
||||||
|
async function fetchNotesForCurrentUser() {
|
||||||
const { $client } = useNuxtApp();
|
const { $client } = useNuxtApp();
|
||||||
const { notes } = await $client.notes.getForCurrentUser.query();
|
const { notes } = await $client.notes.getForCurrentUser.query();
|
||||||
if(notes){
|
if(notes){
|
||||||
this.notes = 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