prettier fixes #16
This commit is contained in:
@@ -4,10 +4,10 @@ import NotesService from '~/lib/services/notes.service';
|
||||
|
||||
// Example API Route with query params ... /api/note?note_id=41
|
||||
export default defineProtectedEventHandler(async (event: H3Event) => {
|
||||
const queryParams = getQuery(event)
|
||||
const queryParams = getQuery(event);
|
||||
let note_id: string = '';
|
||||
if(queryParams.note_id){
|
||||
if (Array.isArray( queryParams.note_id)) {
|
||||
if (queryParams.note_id) {
|
||||
if (Array.isArray(queryParams.note_id)) {
|
||||
note_id = queryParams.note_id[0];
|
||||
} else {
|
||||
note_id = queryParams.note_id.toString();
|
||||
@@ -15,9 +15,9 @@ export default defineProtectedEventHandler(async (event: H3Event) => {
|
||||
}
|
||||
|
||||
const notesService = new NotesService();
|
||||
const note = await notesService.getNoteById(+note_id);
|
||||
const note = await notesService.getNoteById(+note_id);
|
||||
|
||||
return {
|
||||
note,
|
||||
}
|
||||
})
|
||||
note
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* This is the API-handler of your app that contains all your API routes.
|
||||
* On a bigger app, you will probably want to split this file up into multiple files.
|
||||
*/
|
||||
import { createNuxtApiHandler } from "trpc-nuxt";
|
||||
import { createContext } from "~~/server/trpc/context";
|
||||
import { appRouter } from "~~/server/trpc/routers/app.router";
|
||||
import { createNuxtApiHandler } from 'trpc-nuxt';
|
||||
import { createContext } from '~~/server/trpc/context';
|
||||
import { appRouter } from '~~/server/trpc/routers/app.router';
|
||||
|
||||
// export API handler
|
||||
export default createNuxtApiHandler({
|
||||
@@ -12,5 +12,5 @@ export default createNuxtApiHandler({
|
||||
createContext: createContext,
|
||||
onError({ error }) {
|
||||
console.error(error);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventHandler, EventHandlerRequest, H3Event, eventHandler } from "h3";
|
||||
import { EventHandler, EventHandlerRequest, H3Event, eventHandler } from 'h3';
|
||||
|
||||
export const defineProtectedEventHandler = <T extends EventHandlerRequest>(
|
||||
handler: EventHandler<T>
|
||||
@@ -8,7 +8,7 @@ export const defineProtectedEventHandler = <T extends EventHandlerRequest>(
|
||||
return eventHandler((event: H3Event) => {
|
||||
const user = event.context.user;
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 401, statusMessage: "Unauthenticated" });
|
||||
throw createError({ statusCode: 401, statusMessage: 'Unauthenticated' });
|
||||
}
|
||||
return handler(event);
|
||||
});
|
||||
|
||||
@@ -1,49 +1,65 @@
|
||||
import { defineEventHandler, parseCookies, setCookie, getCookie } from 'h3'
|
||||
import { serverSupabaseUser } from "#supabase/server";
|
||||
import { defineEventHandler, parseCookies, setCookie, getCookie } from 'h3';
|
||||
import { serverSupabaseUser } from '#supabase/server';
|
||||
import AuthService from '~/lib/services/auth.service';
|
||||
|
||||
import { User } from '@supabase/supabase-js';
|
||||
import { FullDBUser } from "~~/lib/services/service.types";
|
||||
import { FullDBUser } from '~~/lib/services/service.types';
|
||||
|
||||
// Explicitly type our context by 'Merging' our custom types with the H3EventContext (https://stackoverflow.com/a/76349232/95242)
|
||||
declare module 'h3' {
|
||||
interface H3EventContext {
|
||||
user?: User; // the Supabase User
|
||||
dbUser?: FullDBUser; // the corresponding Database User
|
||||
user?: User; // the Supabase User
|
||||
dbUser?: FullDBUser; // the corresponding Database User
|
||||
activeAccountId?: number; // the account ID that is active for the user
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const cookies = parseCookies(event)
|
||||
if(cookies && cookies['sb-access-token']){
|
||||
export default defineEventHandler(async event => {
|
||||
const cookies = parseCookies(event);
|
||||
if (cookies && cookies['sb-access-token']) {
|
||||
const user = await serverSupabaseUser(event);
|
||||
if (user) {
|
||||
event.context.user = user;
|
||||
|
||||
const authService = new AuthService();
|
||||
let dbUser = await authService.getFullUserBySupabaseId(user.id);
|
||||
|
||||
|
||||
if (!dbUser && user) {
|
||||
dbUser = await authService.createUser(user.id, user.user_metadata.full_name?user.user_metadata.full_name:"no name supplied", user.email?user.email:"no@email.supplied" );
|
||||
dbUser = await authService.createUser(
|
||||
user.id,
|
||||
user.user_metadata.full_name
|
||||
? user.user_metadata.full_name
|
||||
: 'no name supplied',
|
||||
user.email ? user.email : 'no@email.supplied'
|
||||
);
|
||||
console.log(`\n Created DB User \n ${JSON.stringify(dbUser)}\n`);
|
||||
}
|
||||
|
||||
if(dbUser){
|
||||
|
||||
if (dbUser) {
|
||||
event.context.dbUser = dbUser;
|
||||
let activeAccountId;
|
||||
const preferredAccountId = getCookie(event, 'preferred-active-account-id')
|
||||
if(preferredAccountId && dbUser?.memberships.find(m => m.account_id === +preferredAccountId && !m.pending)){
|
||||
activeAccountId = +preferredAccountId
|
||||
const preferredAccountId = getCookie(
|
||||
event,
|
||||
'preferred-active-account-id'
|
||||
);
|
||||
if (
|
||||
preferredAccountId &&
|
||||
dbUser?.memberships.find(
|
||||
m => m.account_id === +preferredAccountId && !m.pending
|
||||
)
|
||||
) {
|
||||
activeAccountId = +preferredAccountId;
|
||||
} else {
|
||||
const defaultActive = dbUser.memberships[0].account_id.toString();
|
||||
setCookie(event, 'preferred-active-account-id', defaultActive, {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10)});
|
||||
setCookie(event, 'preferred-active-account-id', defaultActive, {
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10)
|
||||
});
|
||||
activeAccountId = +defaultActive;
|
||||
}
|
||||
if(activeAccountId){
|
||||
if (activeAccountId) {
|
||||
event.context.activeAccountId = activeAccountId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,20 +6,31 @@ import { AccountWithMembers } from '~~/lib/services/service.types';
|
||||
const config = useRuntimeConfig();
|
||||
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' });
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
let { price_id, account_id} = body;
|
||||
account_id = +account_id
|
||||
console.log(`session.post.ts recieved price_id:${price_id}, account_id:${account_id}`);
|
||||
export default defineEventHandler(async event => {
|
||||
const body = await readBody(event);
|
||||
let { price_id, account_id } = body;
|
||||
account_id = +account_id;
|
||||
console.log(
|
||||
`session.post.ts recieved price_id:${price_id}, account_id:${account_id}`
|
||||
);
|
||||
|
||||
const accountService = new AccountService();
|
||||
const account: AccountWithMembers = await accountService.getAccountById(account_id);
|
||||
let customer_id: string
|
||||
if(!account.stripe_customer_id){
|
||||
const account: AccountWithMembers = await accountService.getAccountById(
|
||||
account_id
|
||||
);
|
||||
let customer_id: string;
|
||||
if (!account.stripe_customer_id) {
|
||||
// need to pre-emptively create a Stripe user for this account so we know who they are when the webhook comes back
|
||||
const owner = account.members.find(member => (member.access == ACCOUNT_ACCESS.OWNER))
|
||||
console.log(`Creating account with name ${account.name} and email ${owner?.user.email}`);
|
||||
const customer = await stripe.customers.create({ name: account.name, email: owner?.user.email });
|
||||
const owner = account.members.find(
|
||||
member => member.access == ACCOUNT_ACCESS.OWNER
|
||||
);
|
||||
console.log(
|
||||
`Creating account with name ${account.name} and email ${owner?.user.email}`
|
||||
);
|
||||
const customer = await stripe.customers.create({
|
||||
name: account.name,
|
||||
email: owner?.user.email
|
||||
});
|
||||
customer_id = customer.id;
|
||||
accountService.updateAccountStipeCustomerId(account_id, customer.id);
|
||||
} else {
|
||||
@@ -31,8 +42,8 @@ export default defineEventHandler(async (event) => {
|
||||
line_items: [
|
||||
{
|
||||
price: price_id,
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
|
||||
// the actual Session ID is returned in the query parameter when your customer
|
||||
@@ -42,11 +53,9 @@ export default defineEventHandler(async (event) => {
|
||||
customer: customer_id
|
||||
});
|
||||
|
||||
if(session?.url){
|
||||
if (session?.url) {
|
||||
return sendRedirect(event, session.url, 303);
|
||||
} else {
|
||||
return sendRedirect(event, `${config.public.siteRootUrl}/fail`, 303);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,46 +4,77 @@ import AccountService from '~~/lib/services/account.service';
|
||||
const config = useRuntimeConfig();
|
||||
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' });
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
export default defineEventHandler(async event => {
|
||||
const stripeSignature = getRequestHeader(event, 'stripe-signature');
|
||||
if(!stripeSignature){
|
||||
throw createError({ statusCode: 400, statusMessage: 'Webhook Error: No stripe signature in header' });
|
||||
if (!stripeSignature) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Webhook Error: No stripe signature in header'
|
||||
});
|
||||
}
|
||||
|
||||
const rawBody = await readRawBody(event)
|
||||
if(!rawBody){
|
||||
throw createError({ statusCode: 400, statusMessage: 'Webhook Error: No body' });
|
||||
const rawBody = await readRawBody(event);
|
||||
if (!rawBody) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Webhook Error: No body'
|
||||
});
|
||||
}
|
||||
let stripeEvent: Stripe.Event;
|
||||
|
||||
try {
|
||||
stripeEvent = stripe.webhooks.constructEvent(rawBody, stripeSignature, config.stripeEndpointSecret);
|
||||
}
|
||||
catch (err) {
|
||||
stripeEvent = stripe.webhooks.constructEvent(
|
||||
rawBody,
|
||||
stripeSignature,
|
||||
config.stripeEndpointSecret
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw createError({ statusCode: 400, statusMessage: `Error validating Webhook Event` });
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Error validating Webhook Event`
|
||||
});
|
||||
}
|
||||
|
||||
if(stripeEvent.type && stripeEvent.type.startsWith('customer.subscription')){
|
||||
if (
|
||||
stripeEvent.type &&
|
||||
stripeEvent.type.startsWith('customer.subscription')
|
||||
) {
|
||||
console.log(`****** Web Hook Recieved (${stripeEvent.type}) ******`);
|
||||
|
||||
let subscription = stripeEvent.data.object as Stripe.Subscription;
|
||||
if(subscription.status == 'active'){
|
||||
const sub_item = subscription.items.data.find(item => item?.object && item?.object == 'subscription_item')
|
||||
|
||||
let subscription = stripeEvent.data.object as Stripe.Subscription;
|
||||
if (subscription.status == 'active') {
|
||||
const sub_item = subscription.items.data.find(
|
||||
item => item?.object && item?.object == 'subscription_item'
|
||||
);
|
||||
|
||||
const stripe_product_id = sub_item?.plan.product?.toString(); // TODO - is the product ever a product object and in that case should I check for deleted?
|
||||
if(!stripe_product_id){
|
||||
throw createError({ statusCode: 400, statusMessage: `Error validating Webhook Event` });
|
||||
if (!stripe_product_id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Error validating Webhook Event`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const accountService = new AccountService();
|
||||
|
||||
let current_period_ends: Date = new Date(subscription.current_period_end * 1000);
|
||||
current_period_ends.setDate(current_period_ends.getDate() + config.subscriptionGraceDays);
|
||||
|
||||
console.log(`updating stripe sub details subscription.current_period_end:${subscription.current_period_end}, subscription.id:${subscription.id}, stripe_product_id:${stripe_product_id}`);
|
||||
accountService.updateStripeSubscriptionDetailsForAccount(subscription.customer.toString(), subscription.id, current_period_ends, stripe_product_id);
|
||||
|
||||
let current_period_ends: Date = new Date(
|
||||
subscription.current_period_end * 1000
|
||||
);
|
||||
current_period_ends.setDate(
|
||||
current_period_ends.getDate() + config.subscriptionGraceDays
|
||||
);
|
||||
|
||||
console.log(
|
||||
`updating stripe sub details subscription.current_period_end:${subscription.current_period_end}, subscription.id:${subscription.id}, stripe_product_id:${stripe_product_id}`
|
||||
);
|
||||
accountService.updateStripeSubscriptionDetailsForAccount(
|
||||
subscription.customer.toString(),
|
||||
subscription.id,
|
||||
current_period_ends,
|
||||
stripe_product_id
|
||||
);
|
||||
}
|
||||
}
|
||||
return `handled ${stripeEvent.type}.`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { inferAsyncReturnType } from '@trpc/server'
|
||||
import { inferAsyncReturnType } from '@trpc/server';
|
||||
import { H3Event } from 'h3';
|
||||
|
||||
export async function createContext(event: H3Event){
|
||||
export async function createContext(event: H3Event) {
|
||||
return {
|
||||
user: event.context.user, // the Supabase User
|
||||
dbUser: event.context.dbUser, // the corresponding Database User
|
||||
user: event.context.user, // the Supabase User
|
||||
dbUser: event.context.dbUser, // the corresponding Database User
|
||||
activeAccountId: event.context.activeAccountId, // the account ID that is active for the user
|
||||
event, // required to enable setCookie in accountRouter
|
||||
}
|
||||
};
|
||||
event // required to enable setCookie in accountRouter
|
||||
};
|
||||
}
|
||||
|
||||
export type Context = inferAsyncReturnType<typeof createContext>
|
||||
export type Context = inferAsyncReturnType<typeof createContext>;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { router, adminProcedure, publicProcedure, protectedProcedure, ownerProcedure } from '../trpc'
|
||||
import {
|
||||
router,
|
||||
adminProcedure,
|
||||
publicProcedure,
|
||||
protectedProcedure,
|
||||
ownerProcedure
|
||||
} from '../trpc';
|
||||
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
|
||||
import { z } from 'zod';
|
||||
import AccountService from '~~/lib/services/account.service';
|
||||
@@ -9,113 +15,161 @@ import { MembershipWithAccount } from '~~/lib/services/service.types';
|
||||
Note on proliferation of Bang syntax... adminProcedure throws if either the ctx.dbUser or the ctx.activeAccountId is not available but the compiler can't figure that out so bang quiesces the null warning
|
||||
*/
|
||||
export const accountRouter = router({
|
||||
getDBUser: publicProcedure
|
||||
.query(({ ctx }) => {
|
||||
return {
|
||||
dbUser: ctx.dbUser,
|
||||
}
|
||||
}),
|
||||
getActiveAccountId: publicProcedure
|
||||
.query(({ ctx }) => {
|
||||
return {
|
||||
activeAccountId: ctx.activeAccountId,
|
||||
}
|
||||
}),
|
||||
getDBUser: publicProcedure.query(({ ctx }) => {
|
||||
return {
|
||||
dbUser: ctx.dbUser
|
||||
};
|
||||
}),
|
||||
getActiveAccountId: publicProcedure.query(({ ctx }) => {
|
||||
return {
|
||||
activeAccountId: ctx.activeAccountId
|
||||
};
|
||||
}),
|
||||
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` });
|
||||
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)});
|
||||
setCookie(
|
||||
ctx.event,
|
||||
'preferred-active-account-id',
|
||||
input.account_id.toString(),
|
||||
{ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10) }
|
||||
);
|
||||
}),
|
||||
changeAccountName: adminProcedure
|
||||
.input(z.object({ new_name: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const accountService = new AccountService();
|
||||
const account = await accountService.changeAccountName(ctx.activeAccountId!, input.new_name);
|
||||
const account = await accountService.changeAccountName(
|
||||
ctx.activeAccountId!,
|
||||
input.new_name
|
||||
);
|
||||
return {
|
||||
account,
|
||||
}
|
||||
}),
|
||||
rotateJoinPassword: adminProcedure
|
||||
.mutation(async ({ ctx }) => {
|
||||
const accountService = new AccountService();
|
||||
const account = await accountService.rotateJoinPassword(ctx.activeAccountId!);
|
||||
return {
|
||||
account,
|
||||
}
|
||||
account
|
||||
};
|
||||
}),
|
||||
rotateJoinPassword: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const accountService = new AccountService();
|
||||
const account = await accountService.rotateJoinPassword(
|
||||
ctx.activeAccountId!
|
||||
);
|
||||
return {
|
||||
account
|
||||
};
|
||||
}),
|
||||
getAccountByJoinPassword: publicProcedure
|
||||
.input(z.object({ join_password: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
const accountService = new AccountService();
|
||||
const account = await accountService.getAccountByJoinPassword(input.join_password);
|
||||
const account = await accountService.getAccountByJoinPassword(
|
||||
input.join_password
|
||||
);
|
||||
return {
|
||||
account,
|
||||
}
|
||||
account
|
||||
};
|
||||
}),
|
||||
joinUserToAccountPending: publicProcedure // this uses a passed account id rather than using the active account because user is usually active on their personal or some other account when they attempt to join a new account
|
||||
.input(z.object({ account_id: z.number(), user_id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const accountService = new AccountService();
|
||||
const membership: MembershipWithAccount = await accountService.joinUserToAccount(input.user_id, input.account_id, true);
|
||||
const membership: MembershipWithAccount =
|
||||
await accountService.joinUserToAccount(
|
||||
input.user_id,
|
||||
input.account_id,
|
||||
true
|
||||
);
|
||||
return {
|
||||
membership,
|
||||
}
|
||||
membership
|
||||
};
|
||||
}),
|
||||
acceptPendingMembership: adminProcedure
|
||||
.input(z.object({ membership_id: z.number() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const accountService = new AccountService();
|
||||
const membership: MembershipWithAccount = await accountService.acceptPendingMembership(ctx.activeAccountId!, input.membership_id);
|
||||
const membership: MembershipWithAccount =
|
||||
await accountService.acceptPendingMembership(
|
||||
ctx.activeAccountId!,
|
||||
input.membership_id
|
||||
);
|
||||
return {
|
||||
membership,
|
||||
}
|
||||
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);
|
||||
const membership: MembershipWithAccount =
|
||||
await accountService.deleteMembership(
|
||||
ctx.activeAccountId!,
|
||||
input.membership_id
|
||||
);
|
||||
return {
|
||||
membership,
|
||||
}
|
||||
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);
|
||||
const membership: MembershipWithAccount =
|
||||
await accountService.deleteMembership(
|
||||
ctx.activeAccountId!,
|
||||
input.membership_id
|
||||
);
|
||||
return {
|
||||
membership,
|
||||
}
|
||||
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]) }))
|
||||
.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 }) => {
|
||||
const accountService = new AccountService();
|
||||
const membership = await accountService.changeUserAccessWithinAccount(input.user_id, ctx.activeAccountId!, input.access);
|
||||
const membership = await accountService.changeUserAccessWithinAccount(
|
||||
input.user_id,
|
||||
ctx.activeAccountId!,
|
||||
input.access
|
||||
);
|
||||
return {
|
||||
membership,
|
||||
}
|
||||
membership
|
||||
};
|
||||
}),
|
||||
claimOwnershipOfAccount: adminProcedure
|
||||
.mutation(async ({ ctx }) => {
|
||||
const accountService = new AccountService();
|
||||
const memberships = await accountService.claimOwnershipOfAccount(ctx.dbUser!.id, ctx.activeAccountId!);
|
||||
return {
|
||||
memberships,
|
||||
}
|
||||
}),
|
||||
getAccountMembers: adminProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const accountService = new AccountService();
|
||||
const memberships = await accountService.getAccountMembers(ctx.activeAccountId!);
|
||||
return {
|
||||
memberships,
|
||||
}
|
||||
}),
|
||||
})
|
||||
claimOwnershipOfAccount: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const accountService = new AccountService();
|
||||
const memberships = await accountService.claimOwnershipOfAccount(
|
||||
ctx.dbUser!.id,
|
||||
ctx.activeAccountId!
|
||||
);
|
||||
return {
|
||||
memberships
|
||||
};
|
||||
}),
|
||||
getAccountMembers: adminProcedure.query(async ({ ctx }) => {
|
||||
const accountService = new AccountService();
|
||||
const memberships = await accountService.getAccountMembers(
|
||||
ctx.activeAccountId!
|
||||
);
|
||||
return {
|
||||
memberships
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { router } from "~/server/trpc/trpc";
|
||||
import { notesRouter } from "./notes.router";
|
||||
import { authRouter } from "./auth.router";
|
||||
import { accountRouter } from "./account.router";
|
||||
import { router } from '~/server/trpc/trpc';
|
||||
import { notesRouter } from './notes.router';
|
||||
import { authRouter } from './auth.router';
|
||||
import { accountRouter } from './account.router';
|
||||
|
||||
export const appRouter = router({
|
||||
notes: notesRouter,
|
||||
auth: authRouter,
|
||||
account: accountRouter,
|
||||
account: accountRouter
|
||||
});
|
||||
|
||||
// export only the type definition of the API
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { publicProcedure, router } from '../trpc'
|
||||
import { publicProcedure, router } from '../trpc';
|
||||
|
||||
export const authRouter = router({
|
||||
getDBUser: publicProcedure
|
||||
.query(({ ctx }) => {
|
||||
return {
|
||||
dbUser: ctx.dbUser,
|
||||
}
|
||||
}),
|
||||
})
|
||||
getDBUser: publicProcedure.query(({ ctx }) => {
|
||||
return {
|
||||
dbUser: ctx.dbUser
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,50 +1,68 @@
|
||||
import NotesService from '~~/lib/services/notes.service';
|
||||
import { accountHasSpecialFeature, adminProcedure, memberProcedure, publicProcedure, readWriteProcedure, router } from '../trpc';
|
||||
import {
|
||||
accountHasSpecialFeature,
|
||||
adminProcedure,
|
||||
memberProcedure,
|
||||
publicProcedure,
|
||||
readWriteProcedure,
|
||||
router
|
||||
} from '../trpc';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const notesRouter = router({
|
||||
getForActiveAccount: memberProcedure
|
||||
.query(async ({ ctx, input }) => {
|
||||
const notesService = new NotesService();
|
||||
const notes = (ctx.activeAccountId)?await notesService.getNotesForAccountId(ctx.activeAccountId):[];
|
||||
return {
|
||||
notes,
|
||||
}
|
||||
}),
|
||||
getForActiveAccount: memberProcedure.query(async ({ ctx, input }) => {
|
||||
const notesService = new NotesService();
|
||||
const notes = ctx.activeAccountId
|
||||
? await notesService.getNotesForAccountId(ctx.activeAccountId)
|
||||
: [];
|
||||
return {
|
||||
notes
|
||||
};
|
||||
}),
|
||||
getById: publicProcedure
|
||||
.input(z.object({ note_id: z.number() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const notesService = new NotesService();
|
||||
const note = await notesService.getNoteById(input.note_id);
|
||||
const note = await notesService.getNoteById(input.note_id);
|
||||
return {
|
||||
note,
|
||||
}
|
||||
note
|
||||
};
|
||||
}),
|
||||
createNote: readWriteProcedure
|
||||
.input(z.object({ note_text: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const notesService = new NotesService();
|
||||
const note = (ctx.activeAccountId)?await notesService.createNote(ctx.activeAccountId, input.note_text):null;
|
||||
const note = ctx.activeAccountId
|
||||
? await notesService.createNote(ctx.activeAccountId, input.note_text)
|
||||
: null;
|
||||
return {
|
||||
note,
|
||||
}
|
||||
note
|
||||
};
|
||||
}),
|
||||
deleteNote: adminProcedure
|
||||
.input(z.object({ note_id: z.number() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const notesService = new NotesService();
|
||||
const note = (ctx.activeAccountId)?await notesService.deleteNote(input.note_id):null;
|
||||
const note = ctx.activeAccountId
|
||||
? await notesService.deleteNote(input.note_id)
|
||||
: null;
|
||||
return {
|
||||
note,
|
||||
}
|
||||
note
|
||||
};
|
||||
}),
|
||||
generateAINoteFromPrompt: readWriteProcedure.use(accountHasSpecialFeature)
|
||||
generateAINoteFromPrompt: readWriteProcedure
|
||||
.use(accountHasSpecialFeature)
|
||||
.input(z.object({ user_prompt: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const notesService = new NotesService();
|
||||
const noteText = (ctx.activeAccountId)?await notesService.generateAINoteFromPrompt(input.user_prompt, ctx.activeAccountId):null;
|
||||
const noteText = ctx.activeAccountId
|
||||
? await notesService.generateAINoteFromPrompt(
|
||||
input.user_prompt,
|
||||
ctx.activeAccountId
|
||||
)
|
||||
: null;
|
||||
return {
|
||||
noteText
|
||||
}
|
||||
}),
|
||||
})
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @see https://trpc.io/docs/v10/router
|
||||
* @see https://trpc.io/docs/v10/procedures
|
||||
*/
|
||||
import { initTRPC, TRPCError } from '@trpc/server'
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import { Context } from './context';
|
||||
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
|
||||
import superjson from 'superjson';
|
||||
@@ -15,7 +15,7 @@ import { AccountLimitError } from '~~/lib/services/errors';
|
||||
|
||||
const t = initTRPC.context<Context>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter: (opts)=> {
|
||||
errorFormatter: opts => {
|
||||
const { shape, error } = opts;
|
||||
if (!(error.cause instanceof AccountLimitError)) {
|
||||
return shape;
|
||||
@@ -26,10 +26,10 @@ const t = initTRPC.context<Context>().create({
|
||||
...shape.data,
|
||||
httpStatus: 401,
|
||||
code: 'UNAUTHORIZED'
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* auth middlewares
|
||||
@@ -40,58 +40,97 @@ const isAuthed = t.middleware(({ next, ctx }) => {
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
user: ctx.user,
|
||||
},
|
||||
user: ctx.user
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const isMemberWithAccessesForActiveAccountId = (access: ACCOUNT_ACCESS[]) =>
|
||||
t.middleware(({ next, ctx }) => {
|
||||
if (!ctx.dbUser || !ctx.activeAccountId) {
|
||||
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 isMemberWithAccessesForActiveAccountId = (access: ACCOUNT_ACCESS[]) => t.middleware(({ next, ctx }) => {
|
||||
if (!ctx.dbUser || !ctx.activeAccountId) {
|
||||
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}`
|
||||
);
|
||||
|
||||
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) {
|
||||
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 pending approval`
|
||||
});
|
||||
}
|
||||
|
||||
if(activeMembership.pending) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message:`membership ${activeMembership?.id} is pending approval` });
|
||||
}
|
||||
if (access.length > 0 && !access.includes(activeMembership.access)) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: `activeMembership ${activeMembership?.id} has insufficient access (${activeMembership?.access})`
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
return 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);
|
||||
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
|
||||
);
|
||||
|
||||
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 });
|
||||
});
|
||||
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 });
|
||||
});
|
||||
|
||||
/**
|
||||
* Procedures
|
||||
**/
|
||||
export const publicProcedure = t.procedure;
|
||||
export const protectedProcedure = t.procedure.use(isAuthed);
|
||||
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 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;
|
||||
|
||||
Reference in New Issue
Block a user