diff --git a/README.md b/README.md index 28d5ec6..6133c4a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Please don't hitch your wagon to this star just yet... I'm coding this in the op - [x] Gen/Regen an invite link to allow users to join a team - [x] Team administrators and owners can accept pending invites - [x] Team administrators and owners can administer the permissions (roles) of other team members on the Accounts page -- [ ] Team owners can remove users from team +- [x] Team owners can remove users from team ### Plans and Pricing - [x] Manage multiple Plans each with specific Feature flags and Plan limits @@ -63,7 +63,6 @@ Please don't hitch your wagon to this star just yet... I'm coding this in the op ### Look and Feel, Design System and Customisation - [x] Default UI isn't too crap - [x] Integrated Design system including theming (Tailwind + daisyUI) -- [ ] Branding options (logo, color scheme, etc.) ### Demo Software (Notes) - [x] Simple Text based Notes functionality @@ -73,10 +72,6 @@ Please don't hitch your wagon to this star just yet... I'm coding this in the op - [x] Max Notes enforced - [x] Add, Delete notes on Dashboard -### Mobile App -- [ ] Flutter App Demo integrating with API endpoints, Auth etc -- [ ] Mobile-friendly web interface. - ### Testing - [ ] Unit tests for server functions - [ ] Integration tests around subscription scenarios diff --git a/components/AppHeader.vue b/components/AppHeader.vue index d798c0d..a180868 100644 --- a/components/AppHeader.vue +++ b/components/AppHeader.vue @@ -52,11 +52,12 @@
diff --git a/lib/services/account.service.ts b/lib/services/account.service.ts index c7df7ce..261af58 100644 --- a/lib/services/account.service.ts +++ b/lib/services/account.service.ts @@ -92,6 +92,25 @@ export default class AccountService { }) } + async deleteMembership(account_id: number, membership_id: number): PromiseClick below to request to Join the team. diff --git a/server/trpc/routers/account.router.ts b/server/trpc/routers/account.router.ts index 14ee53b..a2d2a17 100644 --- a/server/trpc/routers/account.router.ts +++ b/server/trpc/routers/account.router.ts @@ -1,4 +1,5 @@ -import { router, adminProcedure, publicProcedure, protectedProcedure } from '../trpc' +import { TRPCError } from '@trpc/server'; +import { router, adminProcedure, publicProcedure, protectedProcedure, ownerProcedure } from '../trpc' import { ACCOUNT_ACCESS } from '@prisma/client'; import { z } from 'zod'; import AccountService from '~~/lib/services/account.service'; @@ -23,6 +24,10 @@ export const accountRouter = router({ changeActiveAccount: protectedProcedure .input(z.object({ account_id: z.number() })) .mutation(async ({ ctx, input }) => { + const activeMembership = ctx.dbUser?.memberships.find(membership => membership.account_id == input.account_id); + if(activeMembership?.pending){ + throw new TRPCError({ code: 'BAD_REQUEST', message:`membership ${activeMembership?.id} is not active so cannot be switched to` }); + } 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)}); }), @@ -70,6 +75,24 @@ export const accountRouter = router({ membership, } }), + rejectPendingMembership: adminProcedure + .input(z.object({ membership_id: z.number() })) + .query(async ({ ctx, input }) => { + const accountService = new AccountService(); + const membership: MembershipWithAccount = await accountService.deleteMembership(ctx.activeAccountId!, input.membership_id); + return { + membership, + } + }), + deleteMembership: ownerProcedure + .input(z.object({ membership_id: z.number() })) + .query(async ({ ctx, input }) => { + const accountService = new AccountService(); + const membership: MembershipWithAccount = await accountService.deleteMembership(ctx.activeAccountId!, input.membership_id); + return { + membership, + } + }), changeUserAccessWithinAccount: adminProcedure .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 }) => { diff --git a/server/trpc/routers/notes.router.ts b/server/trpc/routers/notes.router.ts index 391dfb5..009738a 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 { protectedProcedure, publicProcedure, router } from '../trpc'; +import { memberProcedure, protectedProcedure, publicProcedure, router } from '../trpc'; import { z } from 'zod'; export const notesRouter = router({ - getForCurrentUser: protectedProcedure + getForCurrentUser: 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: protectedProcedure + createNote: memberProcedure .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: protectedProcedure + deleteNote: memberProcedure .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 7d080ba..6f7e8a7 100644 --- a/server/trpc/trpc.ts +++ b/server/trpc/trpc.ts @@ -30,6 +30,18 @@ const isAuthed = t.middleware(({ next, ctx }) => { }); }); +const isMemberForInputAccountId = t.middleware(({ next, rawInput, 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) { + throw new TRPCError({ code: 'UNAUTHORIZED', message:`membership ${activeMembership?.id} is not active` }); + } + + return next({ ctx }); +}); + const isAdminForInputAccountId = t.middleware(({ next, rawInput, ctx }) => { if (!ctx.dbUser || !ctx.activeAccountId) { throw new TRPCError({ code: 'UNAUTHORIZED' }); @@ -42,11 +54,25 @@ const isAdminForInputAccountId = t.middleware(({ next, rawInput, ctx }) => { return next({ ctx }); }); +const isOwnerForInputAccountId = t.middleware(({ next, rawInput, 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}` }); + } + + return next({ ctx }); +}); + /** * Procedures **/ 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 router = t.router; export const middleware = t.middleware; diff --git a/stores/account.store.ts b/stores/account.store.ts index f3907fe..26bb723 100644 --- a/stores/account.store.ts +++ b/stores/account.store.ts @@ -96,6 +96,22 @@ export const useAccountStore = defineStore('account', { } } }, + async rejectPendingMembership(membership_id: number){ + const { $client } = useNuxtApp(); + const { data: membership } = await $client.account.rejectPendingMembership.useQuery({ membership_id }); + + if(membership.value){ + this.activeAccountMembers = this.activeAccountMembers.filter(m => m.id !== membership_id); + } + }, + async deleteMembership(membership_id: number){ + const { $client } = useNuxtApp(); + const { data: membership } = await $client.account.deleteMembership.useQuery({ membership_id }); + + if(membership.value){ + this.activeAccountMembers = this.activeAccountMembers.filter(m => m.id !== membership_id); + } + }, async rotateJoinPassword(){ const { $client } = useNuxtApp(); const { account } = await $client.account.rotateJoinPassword.mutate();