authorisation approach using trpc base procedures
This commit is contained in:
@@ -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] 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] 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.
|
- [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
|
### 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.
|
- [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.
|
||||||
|
|||||||
@@ -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">
|
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
|
Add
|
||||||
</button>
|
</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">
|
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
|
Gen
|
||||||
<Icon name="mdi:magic" class="h-6 w-6"/>
|
<Icon name="mdi:magic" class="h-6 w-6"/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import NotesService from '~~/lib/services/notes.service';
|
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';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const notesRouter = router({
|
export const notesRouter = router({
|
||||||
@@ -38,7 +38,7 @@ export const notesRouter = router({
|
|||||||
note,
|
note,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
generateAINoteFromPrompt: readWriteProcedure
|
generateAINoteFromPrompt: readWriteProcedure.use(accountHasSpecialFeature)
|
||||||
.input(z.object({ user_prompt: z.string() }))
|
.input(z.object({ user_prompt: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const notesService = new NotesService();
|
const notesService = new NotesService();
|
||||||
|
|||||||
@@ -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) {
|
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);
|
const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId);
|
||||||
|
|
||||||
|
console.log(`isMemberWithAccessesForActiveAccountId(${access}) activeMembership?.access:${activeMembership?.access}`);
|
||||||
|
|
||||||
if(!activeMembership) {
|
if(!activeMembership) {
|
||||||
throw new TRPCError({ code: 'UNAUTHORIZED', message:`user is not a member of the active account` });
|
throw new TRPCError({ code: 'UNAUTHORIZED', message:`user is not a member of the active account` });
|
||||||
}
|
}
|
||||||
|
|
||||||
if(activeMembership.pending) {
|
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) {
|
if(access.length > 0 && !access.includes(activeMembership.access)) {
|
||||||
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})` });
|
throw new TRPCError({ code: 'UNAUTHORIZED', message:`activeMembership ${activeMembership?.id} has insufficient access (${activeMembership?.access})` });
|
||||||
}
|
}
|
||||||
|
|
||||||
return next({ ctx });
|
return next({ ctx });
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAdminForActiveAccountId = t.middleware(({ next, ctx }) => {
|
export const isAccountWithFeature = (feature: string) => t.middleware(({ next, ctx }) => {
|
||||||
if (!ctx.dbUser || !ctx.activeAccountId) {
|
if (!ctx.dbUser || !ctx.activeAccountId) {
|
||||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||||
}
|
}
|
||||||
const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId);
|
const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId);
|
||||||
|
|
||||||
if(!activeMembership) {
|
console.log(`isAccountWithFeature(${feature}) activeMembership?.account.features:${activeMembership?.account.features}`);
|
||||||
throw new TRPCError({ code: 'UNAUTHORIZED', message:`user is not a member of the active account` });
|
if(!activeMembership?.account.features.includes(feature)){
|
||||||
}
|
throw new TRPCError({ code: 'UNAUTHORIZED', message: `Account does not have the ${feature} feature` });
|
||||||
|
|
||||||
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 });
|
return next({ ctx });
|
||||||
@@ -116,9 +73,11 @@ const isOwnerForActiveAccountId = t.middleware(({ next, ctx }) => {
|
|||||||
**/
|
**/
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
export const protectedProcedure = t.procedure.use(isAuthed);
|
export const protectedProcedure = t.procedure.use(isAuthed);
|
||||||
export const memberProcedure = protectedProcedure.use(isMemberForActiveAccountId);
|
export const memberProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([]));
|
||||||
export const readWriteProcedure = protectedProcedure.use(isReadWriteForActiveAccountId);
|
export const readWriteProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.READ_WRITE, ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER]));
|
||||||
export const adminProcedure = protectedProcedure.use(isAdminForActiveAccountId);
|
export const adminProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER]));
|
||||||
export const ownerProcedure = protectedProcedure.use(isOwnerForActiveAccountId);
|
export const ownerProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.OWNER]));
|
||||||
|
export const accountHasSpecialFeature = isAccountWithFeature('SPECIAL_FEATURE');
|
||||||
|
|
||||||
export const router = t.router;
|
export const router = t.router;
|
||||||
export const middleware = t.middleware;
|
export const middleware = t.middleware;
|
||||||
|
|||||||
Reference in New Issue
Block a user