authorisation approach using trpc base procedures

This commit is contained in:
Michael Dausmann
2023-04-29 20:32:23 +10:00
parent 3e3c5e57d7
commit c44d433aac
4 changed files with 22 additions and 62 deletions

View File

@@ -50,7 +50,8 @@ Please don't hitch your wagon to this star just yet... I'm coding this in the op
- [x] Plan features copied to Accounts upon successfull subscription
- [x] Loose coupling between Plan and Account Features to allow ad-hoc account tweaks without creating custom plans
- [x] Pricing page appropriately reacts to users with/without account and current plan.
- [ ] Plan features and Limits available in an object structure in Server methods and with method annotations or similar
- [x] User Access level available at the router layer as procedures allowing restriction of access based on user access
- [x] Account features available at the router layer as utility procedures allowing restriction of access based on account features
### Stripe (Payments) Integration
- [x] Each plan is configured with Stripe Product ID so that multiple Stripe Prices can be created for each plan but subscriptions (via Webhook) will still activate the correct plan.

View File

@@ -40,7 +40,7 @@
class="px-4 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
Add
</button>
<button @click.prevent="genNote()" type="button"
<button v-if="activeMembership.account.features.includes('SPECIAL_FEATURE')" @click.prevent="genNote()" type="button"
class="px-4 py-2 text-white bg-orange-500 rounded-md hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-opacity-50">
Gen
<Icon name="mdi:magic" class="h-6 w-6"/>

View File

@@ -1,5 +1,5 @@
import NotesService from '~~/lib/services/notes.service';
import { adminProcedure, memberProcedure, protectedProcedure, publicProcedure, readWriteProcedure, router } from '../trpc';
import { accountHasSpecialFeature, adminProcedure, memberProcedure, publicProcedure, readWriteProcedure, router } from '../trpc';
import { z } from 'zod';
export const notesRouter = router({
@@ -38,7 +38,7 @@ export const notesRouter = router({
note,
}
}),
generateAINoteFromPrompt: readWriteProcedure
generateAINoteFromPrompt: readWriteProcedure.use(accountHasSpecialFeature)
.input(z.object({ user_prompt: z.string() }))
.query(async ({ ctx, input }) => {
const notesService = new NotesService();

View File

@@ -30,82 +30,39 @@ const isAuthed = t.middleware(({ next, 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 }) => {
const isMemberWithAccessesForActiveAccountId = (access: ACCOUNT_ACCESS[]) => t.middleware(({ next, ctx }) => {
if (!ctx.dbUser || !ctx.activeAccountId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'no user or active account information was found' });
}
const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId);
console.log(`isMemberWithAccessesForActiveAccountId(${access}) activeMembership?.access:${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` });
throw new TRPCError({ code: 'UNAUTHORIZED', message:`membership ${activeMembership?.id} is pending approval` });
}
return next({ 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) {
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) {
if(access.length > 0 && !access.includes(activeMembership.access)) {
throw new TRPCError({ code: 'UNAUTHORIZED', message:`activeMembership ${activeMembership?.id} has insufficient access (${activeMembership?.access})` });
}
return next({ ctx });
});
const isAdminForActiveAccountId = t.middleware(({ next, ctx }) => {
export const isAccountWithFeature = (feature: string) => 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.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})` });
console.log(`isAccountWithFeature(${feature}) activeMembership?.account.features:${activeMembership?.account.features}`);
if(!activeMembership?.account.features.includes(feature)){
throw new TRPCError({ code: 'UNAUTHORIZED', message: `Account does not have the ${feature} feature` });
}
return next({ ctx });
@@ -116,9 +73,11 @@ const isOwnerForActiveAccountId = t.middleware(({ next, ctx }) => {
**/
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);
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 memberProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([]));
export const readWriteProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.READ_WRITE, ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER]));
export const adminProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER]));
export const ownerProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.OWNER]));
export const accountHasSpecialFeature = isAccountWithFeature('SPECIAL_FEATURE');
export const router = t.router;
export const middleware = t.middleware;