From 5d21a5731b76b7a5b62a1332b659000fe9790655 Mon Sep 17 00:00:00 2001 From: Michael Dausmann Date: Tue, 25 Apr 2023 15:01:23 +1000 Subject: [PATCH] Main auth and account flows now tested and working --- assets/images/avatar.svg | 14 +++++ components/AppHeader.vue | 4 +- lib/services/account.service.ts | 15 +++--- pages/account.vue | 10 ++++ pages/dashboard.vue | 13 +++-- pages/signup.vue | 10 +++- pages/success.vue | 2 +- server/trpc/routers/account.router.ts | 4 +- server/trpc/routers/notes.router.ts | 8 +-- server/trpc/trpc.ts | 68 +++++++++++++++++++---- stores/account.store.ts | 15 ++---- stores/notes.store.ts | 2 +- test/TEST.md | 77 ++++++++++++++++++++++++--- 13 files changed, 192 insertions(+), 50 deletions(-) create mode 100644 assets/images/avatar.svg diff --git a/assets/images/avatar.svg b/assets/images/avatar.svg new file mode 100644 index 0000000..ebd9dd7 --- /dev/null +++ b/assets/images/avatar.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/components/AppHeader.vue b/components/AppHeader.vue index a180868..1cf50cc 100644 --- a/components/AppHeader.vue +++ b/components/AppHeader.vue @@ -45,8 +45,8 @@ +
+ Maximum Members: + {{ activeMembership?.account.max_members }} +
+
Access Level: {{ activeMembership?.access }} +
diff --git a/pages/dashboard.vue b/pages/dashboard.vue index 4597417..83d025e 100644 --- a/pages/dashboard.vue +++ b/pages/dashboard.vue @@ -1,10 +1,13 @@ @@ -25,9 +29,10 @@
-
+
-
@@ -36,7 +41,9 @@

{{ note.note_text }}

-
diff --git a/pages/signup.vue b/pages/signup.vue index 5671341..1a166d0 100644 --- a/pages/signup.vue +++ b/pages/signup.vue @@ -6,12 +6,18 @@ const email = ref('') const password = ref('') const confirmPassword = ref('') + const signUpOk = ref(false) const handleStandardSignup = async () => { try { loading.value = true const { data, error } = await supabase.auth.signUp({ email: email.value, password: password.value }) - if (error) throw error + if (error) { + throw error + } + else { + signUpOk.value = true; + } } catch (error) { alert(error) } finally { @@ -47,6 +53,8 @@
+ +

You have successfully signed up. Please check your email for a link to confirm your email address and proceed.

or

\ No newline at end of file diff --git a/server/trpc/routers/account.router.ts b/server/trpc/routers/account.router.ts index a2d2a17..e42b4ff 100644 --- a/server/trpc/routers/account.router.ts +++ b/server/trpc/routers/account.router.ts @@ -105,9 +105,9 @@ export const accountRouter = router({ claimOwnershipOfAccount: adminProcedure .mutation(async ({ ctx }) => { const accountService = new AccountService(); - const membership = await accountService.claimOwnershipOfAccount(ctx.dbUser!.id, ctx.activeAccountId!); + const memberships = await accountService.claimOwnershipOfAccount(ctx.dbUser!.id, ctx.activeAccountId!); return { - membership, + memberships, } }), getAccountMembers: adminProcedure diff --git a/server/trpc/routers/notes.router.ts b/server/trpc/routers/notes.router.ts index 009738a..24c28e3 100644 --- a/server/trpc/routers/notes.router.ts +++ b/server/trpc/routers/notes.router.ts @@ -1,9 +1,9 @@ import NotesService from '~~/lib/services/notes.service'; -import { memberProcedure, protectedProcedure, publicProcedure, router } from '../trpc'; +import { adminProcedure, memberProcedure, protectedProcedure, publicProcedure, readWriteProcedure, router } from '../trpc'; import { z } from 'zod'; export const notesRouter = router({ - getForCurrentUser: memberProcedure + getForActiveAccount: memberProcedure .query(async ({ ctx, input }) => { const notesService = new NotesService(); const notes = (ctx.activeAccountId)?await notesService.getNotesForAccountId(ctx.activeAccountId):[]; @@ -20,7 +20,7 @@ export const notesRouter = router({ note, } }), - createNote: memberProcedure + createNote: readWriteProcedure .input(z.object({ note_text: z.string() })) .mutation(async ({ ctx, input }) => { const notesService = new NotesService(); @@ -29,7 +29,7 @@ export const notesRouter = router({ note, } }), - deleteNote: memberProcedure + deleteNote: adminProcedure .input(z.object({ note_id: z.number() })) .mutation(async ({ ctx, input }) => { const notesService = new NotesService(); diff --git a/server/trpc/trpc.ts b/server/trpc/trpc.ts index 6f7e8a7..e044c02 100644 --- a/server/trpc/trpc.ts +++ b/server/trpc/trpc.ts @@ -30,37 +30,82 @@ const isAuthed = t.middleware(({ next, ctx }) => { }); }); -const isMemberForInputAccountId = t.middleware(({ next, rawInput, ctx }) => { +// Yes, these functions do look very repetitive and could be refactored. If only I was smart enough to understand https://trpc.io/docs/server/procedures#reusable-base-procedures +const isMemberForActiveAccountId = t.middleware(({ next, ctx }) => { if (!ctx.dbUser || !ctx.activeAccountId) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId); - if(!activeMembership || activeMembership.pending) { + + if(!activeMembership) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`user is not a member of the active account` }); + } + + if(activeMembership.pending) { throw new TRPCError({ code: 'UNAUTHORIZED', message:`membership ${activeMembership?.id} is not active` }); } return next({ ctx }); }); -const isAdminForInputAccountId = t.middleware(({ next, rawInput, ctx }) => { +const isReadWriteForActiveAccountId = t.middleware(({ next, ctx }) => { if (!ctx.dbUser || !ctx.activeAccountId) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } 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', message:`activeMembership ${activeMembership?.id} is only ${activeMembership?.access}` }); + + if(!activeMembership) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`user is not a member of the active account` }); + } + + if(activeMembership.pending) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`membership ${activeMembership?.id} is not active` }); + } + + if(activeMembership?.access !== ACCOUNT_ACCESS.READ_WRITE && activeMembership?.access !== ACCOUNT_ACCESS.ADMIN && activeMembership?.access !== ACCOUNT_ACCESS.OWNER) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`activeMembership ${activeMembership?.id} has insufficient access (${activeMembership?.access})` }); } return next({ ctx }); }); -const isOwnerForInputAccountId = t.middleware(({ next, rawInput, ctx }) => { +const isAdminForActiveAccountId = t.middleware(({ next, ctx }) => { if (!ctx.dbUser || !ctx.activeAccountId) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId); - if(!activeMembership || activeMembership?.access !== ACCOUNT_ACCESS.OWNER) { - throw new TRPCError({ code: 'UNAUTHORIZED', message:`activeMembership ${activeMembership?.id} is only ${activeMembership?.access}` }); + + if(!activeMembership) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`user is not a member of the active account` }); + } + + if(activeMembership.pending) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`membership ${activeMembership?.id} is not active` }); + } + + if(activeMembership?.access !== ACCOUNT_ACCESS.ADMIN && activeMembership?.access !== ACCOUNT_ACCESS.OWNER) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`activeMembership ${activeMembership?.id} has insufficient access (${activeMembership?.access})` }); + } + + return next({ ctx }); +}); + +const isOwnerForActiveAccountId = t.middleware(({ next, ctx }) => { + if (!ctx.dbUser || !ctx.activeAccountId) { + throw new TRPCError({ code: 'UNAUTHORIZED' }); + } + const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId); + + if(!activeMembership) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`user is not a member of the active account` }); + } + + if(activeMembership.pending) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`membership ${activeMembership?.id} is not active` }); + } + + if(activeMembership?.access !== ACCOUNT_ACCESS.OWNER) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`activeMembership ${activeMembership?.id} has insufficient access (${activeMembership?.access})` }); } return next({ ctx }); @@ -71,8 +116,9 @@ const isOwnerForInputAccountId = t.middleware(({ next, rawInput, ctx }) => { **/ export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(isAuthed); -export const memberProcedure = protectedProcedure.use(isMemberForInputAccountId); -export const adminProcedure = protectedProcedure.use(isAdminForInputAccountId); -export const ownerProcedure = protectedProcedure.use(isOwnerForInputAccountId); +export const memberProcedure = protectedProcedure.use(isMemberForActiveAccountId); +export const readWriteProcedure = protectedProcedure.use(isReadWriteForActiveAccountId); +export const adminProcedure = protectedProcedure.use(isAdminForActiveAccountId); +export const ownerProcedure = protectedProcedure.use(isOwnerForActiveAccountId); export const router = t.router; export const middleware = t.middleware; diff --git a/stores/account.store.ts b/stores/account.store.ts index 26bb723..6fe99b0 100644 --- a/stores/account.store.ts +++ b/stores/account.store.ts @@ -140,17 +140,10 @@ export const useAccountStore = defineStore('account', { }, async claimOwnershipOfAccount(){ const { $client } = useNuxtApp(); - 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.id){ - m.access = membership.access; - } - } + const { memberships } = await $client.account.claimOwnershipOfAccount.mutate(); + if(memberships){ + this.activeAccountMembers = memberships; + this.activeMembership!.access = ACCOUNT_ACCESS.OWNER } } } diff --git a/stores/notes.store.ts b/stores/notes.store.ts index ae0b4f2..8f712f1 100644 --- a/stores/notes.store.ts +++ b/stores/notes.store.ts @@ -20,7 +20,7 @@ export const useNotesStore = defineStore('notes', () => { async function fetchNotesForCurrentUser() { const { $client } = useNuxtApp(); - const { notes } = await $client.notes.getForCurrentUser.query(); + const { notes } = await $client.notes.getForActiveAccount.query(); if(notes){ _notes.value = notes; } diff --git a/test/TEST.md b/test/TEST.md index 533d7ad..f1d2a6c 100644 --- a/test/TEST.md +++ b/test/TEST.md @@ -1,10 +1,71 @@ # Manual test for Admin Functions Scenario -Pre-condition -User 3 (encumbent id=3) - Owner of own single user account. Admin of Team account -User 4 (noob id = 4) - Owner of own single user account. +## Pre-req +- Site configured for free plan +- Neither User1 or User2 are present in DB +## Main Flow (Happy Path) +This scenario covers most of the Site Auth and Account admin functions. -User 3... -- joins user 4 to team account (expect user is a read only member of team account) -- upgrades user 4 to owner (should fail) -- upgrades user 4 to admin -- claims ownership of team account \ No newline at end of file +(User 1) +- Front page - Get Started +- Signup with google - should drop to dashboard +- Check account page via nav +- Go to pricing page via nav +- Click on 'Subscribe' button under team account +- Fill in Credit card details and sub in Stripe - Should come back to Dashboard page (comes to success page but no customer info??) +- Add a Note or 2 in the Dashboard page - make it clear user1 has entered +- Check Account view - Should be OWNER of this account, max members should be updated to 10 +- Update Team account Name using button +- Copy Join Link +- Signout + +(User 2) +- Open Join Link - Should prompt for signup to new account name +- Signup with email/password - should drop to dashboard (some fucking bullshit error with signin + no avatar link + how the fuck to deal with non confirmed emails) +- Open join link again - should prompt to Join (Note, doing navigateTo and saving a returnURL seems to be difficult in Nuxt) +- Click Join - should redirect to dashboard +- Check 'Switch to' accounts, team account should be (pending) and not clickable +- Sign out + +(User 1) +- Front Page - Sign in - Note Signin page subtly different to signup page, no password conf and no 'if you proceed' warning +- Sign in with google - Should drop to dashboard page +- navigate to Account page +- Look at members, should show User 2 as 'Pending' with approve/reject buttons +- Click approve, should update user item in list and display 'Upgrade to read/write' and 'Delete' + +(User 2) +- Signin -> Dashboard should now show notes but no 'Delete' or 'Add' buttons + +(User 1) +- Signin -> Dashboard +- Navigate to Account Page +- Click 'Upgrade to read/write' - Should update user and now show 'Upgrade to Admin' button +- sign out + +(User 2) +- Signin -> Dashboard - should see 'Add' button now +- Add a Note +- Sign Out + +(User 1) +- Signin -> Dashboard +- Navigate to Account Page +- Click 'Upgrade to Admin' - Should see just the 'Delete' button now +- sign out + +(User 2) +- Signin -> Dashboard - should now see 'Delete' button on notes +- Click on 'Delete' for an existing Note +- Go to Account Page - should now see 'Claim ownership' button next to access +- Click on 'Claim Ownership' - Button should dissappear and member list should be updated - Delete button should be visible against +User 1 +- Click 'Delete' for user 1 +- You are now king of the world +- navigate to Notes, verify you can see/crud notes on dashboard. + +## Unchecked things +- Admin can approve pending membership + +## Alternate Flow (Pricing First) +- Front Page - Pricing +- get started for free (TODO - should be button to go to signup under free plan) \ No newline at end of file