From caf65a48c142d9b53ec3ae32b1fcb7023d805538 Mon Sep 17 00:00:00 2001 From: Michael Dausmann Date: Sat, 9 Sep 2023 00:01:16 +1000 Subject: [PATCH] api (rest) endpoints. closes #11 --- CHANGELOG.md | 3 ++ package-lock.json | 4 +-- package.json | 2 +- server/api/note.ts | 23 +++++++++++++ server/defineProtectedEventHandler.ts | 15 ++++++++ server/middleware/authContext.ts | 49 +++++++++++++++++++++++++++ server/trpc/context.ts | 42 +++-------------------- 7 files changed, 98 insertions(+), 40 deletions(-) create mode 100644 server/api/note.ts create mode 100644 server/defineProtectedEventHandler.ts create mode 100644 server/middleware/authContext.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4abd4d7..4a3cf80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Version 1.2.0 +- 'Lift' auth context into server middleware to support authenticated api (rest) endpoints for alternate clients while still supporting fully typed Trpc context. + ## Version 1.1.0 - Upgrade Prisma to version 5 to improve performance (https://www.prisma.io/docs/guides/upgrade-guides/upgrading-versions/upgrading-to-prisma-5) ``` diff --git a/package-lock.json b/package-lock.json index 54a62e1..725c363 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supanuxt-saas", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "supanuxt-saas", - "version": "1.1.0", + "version": "1.2.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5800ce0..d8baac5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supanuxt-saas", - "version": "1.1.0", + "version": "1.2.0", "author": { "name": "Michael Dausmann", "email": "mdausmann@gmail.com", diff --git a/server/api/note.ts b/server/api/note.ts new file mode 100644 index 0000000..e5defb7 --- /dev/null +++ b/server/api/note.ts @@ -0,0 +1,23 @@ +import { H3Event, getQuery } from 'h3'; +import { defineProtectedEventHandler } from '../defineProtectedEventHandler'; +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) + let note_id: string = ''; + if(queryParams.note_id){ + if (Array.isArray( queryParams.note_id)) { + note_id = queryParams.note_id[0]; + } else { + note_id = queryParams.note_id.toString(); + } + } + + const notesService = new NotesService(); + const note = await notesService.getNoteById(+note_id); + + return { + note, + } +}) \ No newline at end of file diff --git a/server/defineProtectedEventHandler.ts b/server/defineProtectedEventHandler.ts new file mode 100644 index 0000000..0701b46 --- /dev/null +++ b/server/defineProtectedEventHandler.ts @@ -0,0 +1,15 @@ +import { EventHandler, EventHandlerRequest, H3Event, eventHandler } from "h3"; + +export const defineProtectedEventHandler = ( + handler: EventHandler +): EventHandler => { + handler.__is_handler__ = true; + + return eventHandler((event: H3Event) => { + const user = event.context.user; + if (!user) { + throw createError({ statusCode: 401, statusMessage: "Unauthenticated" }); + } + return handler(event); + }); +}; diff --git a/server/middleware/authContext.ts b/server/middleware/authContext.ts new file mode 100644 index 0000000..16aec84 --- /dev/null +++ b/server/middleware/authContext.ts @@ -0,0 +1,49 @@ +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"; + +// 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 + 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']){ + 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" ); + console.log(`\n Created DB User \n ${JSON.stringify(dbUser)}\n`); + } + + 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 + } 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)}); + activeAccountId = +defaultActive; + } + if(activeAccountId){ + event.context.activeAccountId = activeAccountId; + } + } + } + } +}); \ No newline at end of file diff --git a/server/trpc/context.ts b/server/trpc/context.ts index c9ea707..0459c2d 100644 --- a/server/trpc/context.ts +++ b/server/trpc/context.ts @@ -1,44 +1,12 @@ -import { inferAsyncReturnType, TRPCError } from '@trpc/server' +import { inferAsyncReturnType } from '@trpc/server' import { H3Event } from 'h3'; -import { serverSupabaseUser } from '#supabase/server' -import { User } from '@supabase/supabase-js'; -import { FullDBUser } from '~~/lib/services/service.types'; -import AuthService from '~~/lib/services/auth.service'; export async function createContext(event: H3Event){ - let user: User | null = null; - let dbUser: FullDBUser | null = null; - let activeAccountId: number | null = null; - - if (!user) { - user = await serverSupabaseUser(event); - } - if (!dbUser && user) { - const authService = new AuthService(); - 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" ); - console.log(`\n Created DB User \n ${JSON.stringify(dbUser)}\n`); - } - - if(dbUser){ - 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)}); - activeAccountId = +defaultActive; - } - } - } - return { - user, // the Supabase User - dbUser, // the corresponding Database User - activeAccountId, // the account ID that is active for the user - event, // required to enable setCookie in accountRouter + 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 } };