diff --git a/lib/services/account.service.ts b/lib/services/account.service.ts
index 261af58..9cbe5e2 100644
--- a/lib/services/account.service.ts
+++ b/lib/services/account.service.ts
@@ -181,7 +181,7 @@ export default class AccountService {
// User must already be an ADMIN for the Account
// Existing OWNER memberships are downgraded to ADMIN
// In future, some sort of Billing/Stripe tie in here e.g. changing email details on the Account, not sure.
- async claimOwnershipOfAccount(user_id: number, account_id: number) {
+ async claimOwnershipOfAccount(user_id: number, account_id: number): Promise {
const membership = await prisma_client.membership.findUniqueOrThrow({
where: {
user_id_account_id: {
@@ -192,7 +192,7 @@ export default class AccountService {
});
if (membership.access === ACCOUNT_ACCESS.OWNER) {
- return; // already owner
+ throw new Error('BADREQUEST: user is already owner');
} else if (membership.access !== ACCOUNT_ACCESS.ADMIN) {
throw new Error('UNAUTHORISED: only Admins can claim ownership');
}
@@ -219,7 +219,7 @@ export default class AccountService {
}
// finally update the ADMIN member to OWNER
- return prisma_client.membership.update({
+ await prisma_client.membership.update({
where: {
user_id_account_id: {
user_id: user_id,
@@ -229,10 +229,13 @@ export default class AccountService {
data: {
access: ACCOUNT_ACCESS.OWNER,
},
- include: {
- account: true
- }
});
+
+ // return the full membership list because 2 members have changed.
+ return prisma_client.membership.findMany({
+ where: { account_id },
+ ...membershipWithUser
+ });
}
// Upgrade access of a membership. Cannot use this method to upgrade to or downgrade from OWNER access
diff --git a/pages/account.vue b/pages/account.vue
index 5d6f619..d30f5f6 100644
--- a/pages/account.vue
+++ b/pages/account.vue
@@ -60,9 +60,19 @@
{{ activeMembership?.account.max_notes }}
+
+ Maximum Members:
+ {{ activeMembership?.account.max_members }}
+
You have successfully signed up. Please check your email for a link to confirm your email address and proceed.
or
We appreciate your business {{customer.name}}!
It appears your stripe customer information has been deleted!
-
Checkout our reasonable Pricing
+
Go to Your Dashboard
\ 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