diff --git a/pages/join/[join_password].vue b/pages/join/[join_password].vue
index 7cb520f..2cd8ec5 100644
--- a/pages/join/[join_password].vue
+++ b/pages/join/[join_password].vue
@@ -4,8 +4,9 @@
const route = useRoute();
const {join_password} : {join_password?: string} = route.params;
- const { $client } = useNuxtApp();
+ const accountStore = useAccountStore();
+ const { $client } = useNuxtApp();
// this could probably be an elegant destructure here but I lost patience
let account: AccountWithMembers | undefined;
if(join_password){
@@ -16,8 +17,8 @@
const { data: dbUser } = await $client.auth.getDBUser.useQuery();
async function doJoin(){
- if(dbUser.value?.dbUser && account){
- await $client.account.joinUserToAccountPending.useQuery({account_id: account.id, user_id: dbUser.value.dbUser.id});
+ if(account){
+ await accountStore.joinUserToAccountPending(account.id);
} else {
console.log(`Unable to Join`)
}
diff --git a/server/trpc/context.ts b/server/trpc/context.ts
index f36d0bf..c9ea707 100644
--- a/server/trpc/context.ts
+++ b/server/trpc/context.ts
@@ -8,6 +8,7 @@ import AuthService from '~~/lib/services/auth.service';
export async function createContext(event: H3Event){
let user: User | null = null;
let dbUser: FullDBUser | null = null;
+ let activeAccountId: number | null = null;
if (!user) {
user = await serverSupabaseUser(event);
@@ -20,11 +21,24 @@ export async function createContext(event: H3Event){
dbUser = await authService.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(dbUser){
+ const preferredAccountId = getCookie(event, 'preferred-active-account-id')
+ if(preferredAccountId && dbUser?.memberships.find(m => m.account_id === +preferredAccountId && !m.pending)){
+ activeAccountId = +preferredAccountId
+ } else {
+ const defaultActive = dbUser.memberships[0].account_id.toString();
+ setCookie(event, 'preferred-active-account-id', defaultActive, {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10)});
+ activeAccountId = +defaultActive;
+ }
+ }
}
return {
- user,
- dbUser,
+ user, // the Supabase User
+ dbUser, // the corresponding Database User
+ activeAccountId, // the account ID that is active for the user
+ event, // required to enable setCookie in accountRouter
}
};
diff --git a/server/trpc/routers/account.router.ts b/server/trpc/routers/account.router.ts
index bdd63a1..c424207 100644
--- a/server/trpc/routers/account.router.ts
+++ b/server/trpc/routers/account.router.ts
@@ -1,93 +1,96 @@
-import { router, adminProcedure, publicProcedure } from '../trpc'
+import { router, adminProcedure, publicProcedure, protectedProcedure } from '../trpc'
import { ACCOUNT_ACCESS } from '@prisma/client';
import { z } from 'zod';
import AccountService from '~~/lib/services/account.service';
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
+ .query(({ ctx }) => {
+ return {
+ dbUser: ctx.dbUser,
+ }
+ }),
+ getActiveAccountId: protectedProcedure
+ .query(({ ctx }) => {
+ return {
+ activeAccountId: ctx.activeAccountId,
+ }
+ }),
+ changeActiveAccount: adminProcedure
+ .input(z.object({ account_id: z.number() }))
+ .mutation(async ({ ctx, input }) => {
+ ctx.activeAccountId = input.account_id;
+ setCookie(ctx.event, 'preferred-active-account-id', input.account_id.toString(), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10)});
+ }),
changeAccountName: adminProcedure
- .input(z.object({ account_id: z.number(), new_name: z.string() }))
- .query(async ({ ctx, input }) => {
+ .input(z.object({ new_name: z.string() }))
+ .mutation(async ({ ctx, input }) => {
const accountService = new AccountService();
- const account = await accountService.changeAccountName(input.account_id, input.new_name);
-
+ const account = await accountService.changeAccountName(ctx.activeAccountId!, input.new_name);
return {
account,
}
}),
rotateJoinPassword: adminProcedure
- .input(z.object({ account_id: z.number() }))
- .query(async ({ ctx, input }) => {
+ .mutation(async ({ ctx }) => {
const accountService = new AccountService();
- const account = await accountService.rotateJoinPassword(input.account_id);
-
+ const account = await accountService.rotateJoinPassword(ctx.activeAccountId!);
return {
account,
}
}),
getAccountByJoinPassword: publicProcedure
.input(z.object({ join_password: z.string() }))
- .query(async ({ ctx, input }) => {
+ .query(async ({ input }) => {
const accountService = new AccountService();
const account = await accountService.getAccountByJoinPassword(input.join_password);
-
return {
account,
}
}),
- joinUserToAccount: adminProcedure
+ joinUserToAccountPending: publicProcedure // this uses a passed account id rather than using the active account because user is usually active on their personal or some other account when they attempt to join a new account
.input(z.object({ account_id: z.number(), user_id: z.number() }))
- .query(async ({ ctx, input }) => {
+ .mutation(async ({ input }) => {
const accountService = new AccountService();
- const membership: MembershipWithAccount| null = (ctx.dbUser?.id)?await accountService.joinUserToAccount(input.user_id, input.account_id, false):null;
- return {
- membership,
- }
- }),
- joinUserToAccountPending: publicProcedure
- .input(z.object({ account_id: z.number(), user_id: z.number() }))
- .query(async ({ ctx, input }) => {
- const accountService = new AccountService();
- const membership: MembershipWithAccount| null = (ctx.dbUser?.id)?await accountService.joinUserToAccount(input.user_id, input.account_id, true):null;
+ const membership: MembershipWithAccount = await accountService.joinUserToAccount(input.user_id, input.account_id, true);
return {
membership,
}
}),
acceptPendingMembership: adminProcedure
- .input(z.object({ account_id: z.number(), membership_id: z.number() }))
+ .input(z.object({ membership_id: z.number() }))
.query(async ({ ctx, input }) => {
const accountService = new AccountService();
- const membership: MembershipWithAccount| null = (ctx.dbUser?.id)?await accountService.acceptPendingMembership(input.account_id, input.membership_id):null;
+ const membership: MembershipWithAccount = await accountService.acceptPendingMembership(ctx.activeAccountId!, input.membership_id);
return {
membership,
}
}),
changeUserAccessWithinAccount: adminProcedure
- .input(z.object({ user_id: z.number(), account_id: z.number(), access: z.enum([ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER, ACCOUNT_ACCESS.READ_ONLY, ACCOUNT_ACCESS.READ_WRITE]) }))
- .query(async ({ ctx, input }) => {
+ .input(z.object({ user_id: z.number(), access: z.enum([ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER, ACCOUNT_ACCESS.READ_ONLY, ACCOUNT_ACCESS.READ_WRITE]) }))
+ .mutation(async ({ ctx, input }) => {
const accountService = new AccountService();
- const membership = await accountService.changeUserAccessWithinAccount(input.user_id, input.account_id, input.access);
-
+ const membership = await accountService.changeUserAccessWithinAccount(input.user_id, ctx.activeAccountId!, input.access);
return {
membership,
}
}),
- claimOwnershipOfAccount: adminProcedure
- .input(z.object({ account_id: z.number() }))
- .query(async ({ ctx, input }) => {
+ claimOwnershipOfAccount: adminProcedure
+ .mutation(async ({ ctx }) => {
const accountService = new AccountService();
- const membership = await accountService.claimOwnershipOfAccount(ctx.dbUser!.id, input.account_id); // adminProcedure errors if ctx.dbUser is null so bang is ok here
-
+ const membership = await accountService.claimOwnershipOfAccount(ctx.dbUser!.id, ctx.activeAccountId!);
return {
membership,
}
}),
- getAccountMembers: adminProcedure
- .input(z.object({ account_id: z.number() }))
- .query(async ({ ctx, input }) => {
+ getAccountMembers: adminProcedure
+ .query(async ({ ctx }) => {
const accountService = new AccountService();
- const memberships = await accountService.getAccountMembers(input.account_id);
-
+ const memberships = await accountService.getAccountMembers(ctx.activeAccountId!);
return {
memberships,
}
diff --git a/server/trpc/routers/notes.router.ts b/server/trpc/routers/notes.router.ts
index b5fc007..0f0f2c8 100644
--- a/server/trpc/routers/notes.router.ts
+++ b/server/trpc/routers/notes.router.ts
@@ -4,10 +4,9 @@ import { z } from 'zod';
export const notesRouter = router({
getForCurrentUser: protectedProcedure
- .input(z.object({ account_id: z.number() }))
.query(async ({ ctx, input }) => {
const notesService = new NotesService();
- const notes = await notesService.getNotesForAccountId(input.account_id);
+ const notes = (ctx.activeAccountId)?await notesService.getNotesForAccountId(ctx.activeAccountId):[];
return {
notes,
}
diff --git a/server/trpc/trpc.ts b/server/trpc/trpc.ts
index e6727ef..39f5331 100644
--- a/server/trpc/trpc.ts
+++ b/server/trpc/trpc.ts
@@ -9,7 +9,6 @@
*/
import { initTRPC, TRPCError } from '@trpc/server'
import { Context } from './context';
-import { z } from 'zod';
import { ACCOUNT_ACCESS } from '@prisma/client';
import superjson from 'superjson';
@@ -32,14 +31,11 @@ const isAuthed = t.middleware(({ next, ctx }) => {
});
const isAdminForInputAccountId = t.middleware(({ next, rawInput, ctx }) => {
- if (!ctx.dbUser) {
+ if (!ctx.dbUser || !ctx.activeAccountId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
- const result = z.object({ account_id: z.number() }).safeParse(rawInput);
- if (!result.success) throw new TRPCError({ code: 'BAD_REQUEST' });
- const { account_id } = result.data;
- const test_membership = ctx.dbUser.memberships.find(membership => membership.account_id == account_id);
- if(!test_membership || (test_membership?.access !== ACCOUNT_ACCESS.ADMIN && test_membership?.access !== ACCOUNT_ACCESS.OWNER)) {
+ 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' });
}
diff --git a/stores/account.store.ts b/stores/account.store.ts
index 0ced5a7..67c84bc 100644
--- a/stores/account.store.ts
+++ b/stores/account.store.ts
@@ -1,43 +1,87 @@
import { ACCOUNT_ACCESS } from ".prisma/client"
import { defineStore } from "pinia"
-import { MembershipWithUser } from "~~/lib/services/service.types";
+import { FullDBUser, MembershipWithUser } from "~~/lib/services/service.types";
-import { useAuthStore } from './auth.store'
+/*
+This store manages User and Account state including the ActiveAccount
+It is used in the Account administration page and the header due to it's account switching features.
+Note) Other pages don't need this state as the dbUser and activeAccount are available on the TRPC Context
+so that other routers can use them to filter results to the active user and account transparently.
+
+[State Map]
+
+(supabase) user -> dbUser <-- this.dbUser
+ |
+ |
+ membership-----membership------membership* <-- this.dbUser.memberships (*this.activeMembership)
+ | | |
+ account account acccount** <--**(id=this.activeAccountId)
+ |
+ membership-----membership------membership <-- this.activeAccountMembers
+ | | |
+ account account acccount*
+*/
interface State {
+ dbUser: FullDBUser | null,
+ activeAccountId: number | null,
activeAccountMembers: MembershipWithUser[]
}
export const useAccountStore = defineStore('account', {
state: (): State => {
return {
+ dbUser: null,
+ activeAccountId: null,
activeAccountMembers: [],
}
},
+ getters: {
+ activeMembership: (state) => state.dbUser?.memberships.find(m => m.account_id === state.activeAccountId)
+ },
actions: {
- async getActiveAccountMembers(){
- const authStore = useAuthStore();
- if(!authStore.activeMembership) { return; }
+ async init(){
const { $client } = useNuxtApp();
- const { data: members } = await $client.account.getAccountMembers.useQuery({account_id: authStore.activeMembership.account_id});
- if(members.value?.memberships){
- this.activeAccountMembers = members.value?.memberships;
+ if(!this.dbUser){
+ const { dbUser } = await $client.auth.getDBUser.query();
+ if(dbUser){
+ this.dbUser = dbUser;
+ }
+ }
+ if(!this.activeAccountId){
+ const { activeAccountId } = await $client.account.getActiveAccountId.query();
+ if(activeAccountId){
+ this.activeAccountId = activeAccountId;
+ }
+ }
+ if(this.activeAccountMembers.length == 0){
+ await this.getActiveAccountMembers();
}
},
- async changeAccountName(new_name: string){
- const authStore = useAuthStore();
- if(!authStore.activeMembership) { return; }
+ async getActiveAccountMembers(){
const { $client } = useNuxtApp();
- const { data: account } = await $client.account.changeAccountName.useQuery({account_id: authStore.activeMembership.account_id, new_name});
- if(account.value?.account){
- authStore.activeMembership.account = account.value.account;
+ 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
+ await this.getActiveAccountMembers(); // these relate to the active account and need to ber re-fetched
+ },
+ async changeAccountName(new_name: string){
+ if(!this.activeMembership){ return; }
+ const { $client } = useNuxtApp();
+ const { account } = await $client.account.changeAccountName.mutate({ new_name });
+ if(account){
+ this.activeMembership.account.name = account.name;
}
},
async acceptPendingMembership(membership_id: number){
- const authStore = useAuthStore();
- if(!authStore.activeMembership) { return; }
const { $client } = useNuxtApp();
- const { data: membership } = await $client.account.acceptPendingMembership.useQuery({account_id: authStore.activeMembership.account_id, membership_id});
+ const { data: membership } = await $client.account.acceptPendingMembership.useQuery({ membership_id });
if(membership.value && membership.value.membership?.pending === false){
for(const m of this.activeAccountMembers){
@@ -48,46 +92,42 @@ export const useAccountStore = defineStore('account', {
}
},
async rotateJoinPassword(){
- const authStore = useAuthStore();
- if(!authStore.activeMembership) { return; }
const { $client } = useNuxtApp();
- const { data: account } = await $client.account.rotateJoinPassword.useQuery({account_id: authStore.activeMembership.account_id});
- if(account.value?.account){
- authStore.activeMembership.account = account.value.account;
+ const { account } = await $client.account.rotateJoinPassword.mutate();
+ if(account && this.activeMembership){
+ this.activeMembership.account = account;
}
},
- async joinUserToAccount(user_id: number){
- const authStore = useAuthStore();
- if(!authStore.activeMembership) { return; }
+ async joinUserToAccountPending(account_id: number){
+ if(!this.dbUser) { return; }
const { $client } = useNuxtApp();
- const { data: membership } = await $client.account.joinUserToAccount.useQuery({account_id: authStore.activeMembership.account_id, user_id});
- if(membership.value?.membership){
- authStore.activeMembership = membership.value.membership;
+ const { membership } = await $client.account.joinUserToAccountPending.mutate({account_id, user_id: this.dbUser.id});
+ if(membership && this.activeMembership){
+ this.dbUser?.memberships.push(membership);
}
},
async changeUserAccessWithinAccount(user_id: number, access: ACCOUNT_ACCESS){
- const authStore = useAuthStore();
- if(!authStore.activeMembership) { return; }
const { $client } = useNuxtApp();
- const { data: membership } = await $client.account.changeUserAccessWithinAccount.useQuery({account_id: authStore.activeMembership.account_id, user_id, access});
- if(membership.value?.membership){
+ const { membership } = await $client.account.changeUserAccessWithinAccount.mutate({ user_id, access });
+ if(membership){
for(const m of this.activeAccountMembers){
- if(m.id === membership.value?.membership.id){
- m.access = membership.value?.membership.access;
+ if(m.id === membership.id){
+ m.access = membership.access;
}
}
}
},
async claimOwnershipOfAccount(){
- const authStore = useAuthStore();
- if(!authStore.activeMembership) { return; }
const { $client } = useNuxtApp();
- const { data: membership } = await $client.account.claimOwnershipOfAccount.useQuery({account_id: authStore.activeMembership.account_id});
- if(membership.value?.membership){
- authStore.activeMembership.access = membership.value.membership.access;
+ const { membership } = await $client.account.claimOwnershipOfAccount.mutate();
+ if(membership){
+ if(this.activeMembership){
+ this.activeMembership.access = membership.access;
+ }
+
for(const m of this.activeAccountMembers){
- if(m.id === membership.value?.membership.id){
- m.access = membership.value?.membership.access;
+ if(m.id === membership.id){
+ m.access = membership.access;
}
}
}
diff --git a/stores/auth.store.ts b/stores/auth.store.ts
deleted file mode 100644
index 38e5a32..0000000
--- a/stores/auth.store.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { defineStore } from "pinia"
-import { FullDBUser, MembershipWithAccount } from "~~/lib/services/service.types";
-
-interface State {
- dbUser?: FullDBUser
- activeMembership: MembershipWithAccount | null
-}
-
-export const useAuthStore = defineStore('auth', {
- state: (): State => {
- return {
- activeMembership: null
- }
- },
- actions: {
- async initUser() {
- if(!this.dbUser || !this.activeMembership){
- const { $client } = useNuxtApp();
- const { dbUser } = await $client.auth.getDBUser.query();
-
- if(dbUser){
- this.dbUser = dbUser;
- const activeMemberships = dbUser.memberships.filter(m => !m.pending)
- if(activeMemberships.length > 0){
- this.activeMembership = activeMemberships[0];
- }
- }
- }
- },
- async changeActiveMembership(membership: MembershipWithAccount) {
- if(membership !== this.activeMembership){
- this.activeMembership = membership;
- }
- },
- }
-});
diff --git a/stores/notes.store.ts b/stores/notes.store.ts
index 03da9d4..aacd98e 100644
--- a/stores/notes.store.ts
+++ b/stores/notes.store.ts
@@ -1,8 +1,6 @@
import { Note } from ".prisma/client"
import { defineStore } from "pinia"
-import { useAuthStore } from './auth.store'
-
interface State {
notes: Note[]
}
@@ -15,12 +13,8 @@ export const useNotesStore = defineStore('notes', {
},
actions: {
async fetchNotesForCurrentUser() {
- const authStore = useAuthStore();
-
- if(!authStore.activeMembership) { return; }
-
const { $client } = useNuxtApp();
- const { notes } = await $client.notes.getForCurrentUser.query({account_id: authStore.activeMembership.account_id});
+ const { notes } = await $client.notes.getForCurrentUser.query();
if(notes){
this.notes = notes;
}