From 7e0102cce5b5056574740aeec3d68670e441c8eb Mon Sep 17 00:00:00 2001 From: Abigail Young Date: Fri, 16 Feb 2024 14:12:06 -0800 Subject: [PATCH] feat: Create new portal user from JWT request if one doesn't exist (#1209) * create new user if no existing from api call * fix lint error * return error if not a valid token, do not try to create a user * fix logic for auth/unauth --- src/pages/api/graphql.tsx | 2 +- src/pages/api/thirdparty/graphql.tsx | 111 +++++++++++++++++---- src/pages/api/thirdparty/resolvers.test.ts | 17 +++- src/pages/api/thirdparty/resolvers.ts | 22 ++-- 4 files changed, 119 insertions(+), 33 deletions(-) diff --git a/src/pages/api/graphql.tsx b/src/pages/api/graphql.tsx index 4a3f0d5cd..490fec399 100644 --- a/src/pages/api/graphql.tsx +++ b/src/pages/api/graphql.tsx @@ -43,7 +43,7 @@ const getExampleCollection = async () => { return res.data.collection as CollectionRecord } -const clientConnection = async () => { +export const clientConnection = async () => { try { const client = await clientPromise return client diff --git a/src/pages/api/thirdparty/graphql.tsx b/src/pages/api/thirdparty/graphql.tsx index 8f8bc4d6c..ed66e0579 100644 --- a/src/pages/api/thirdparty/graphql.tsx +++ b/src/pages/api/thirdparty/graphql.tsx @@ -2,46 +2,121 @@ import { ApolloServer } from '@apollo/server' import { startServerAndCreateNextHandler } from '@as-integrations/next' import { GraphQLError } from 'graphql' import * as jose from 'jose' +import { gql } from 'graphql-tag' +import { clientConnection } from '../graphql' import ThirdPartyKeystoneAPI from './dataSources/thirdPartyKeystone' import { resolvers } from './resolvers' import { typeDefs } from './schema' import { authMiddleware } from './auth' -import clientPromise from 'lib/mongodb' - +import User from 'models/User' +import { client } from 'lib/keystoneClient' +import { EXAMPLE_COLLECTION_ID } from 'constants/index' +import type { CollectionRecord } from 'types/index' /* Apollo Server */ const server = new ApolloServer({ typeDefs, resolvers, }) -const clientConnection = async () => { - try { - const client = await clientPromise - return client - } catch (e) { - // TODO add alert/logging for failure to connect to mongo - console.error('Error connecting to database', e) - throw e - } +export type ThirdPartyUser = { + userId?: string + // We can add more user properties here in the future, if + // resolvers need access to more information about the user. +} + +// To create a new user, we need the example collection from Keystone +export const getExampleCollection = async () => { + // Request the example collection based on ID + const res = await client.query({ + query: gql` + query getCollection($where: CollectionWhereUniqueInput!) { + collection(where: $where) { + id + title + bookmarks { + id + url + label + } + } + } + `, + variables: { + where: { + id: EXAMPLE_COLLECTION_ID, + }, + }, + }) + + return res.data.collection as CollectionRecord } /* Next.js Handler */ export default startServerAndCreateNextHandler(server, { context: async (req) => { const { cache } = server - // Check for JWT + let user: ThirdPartyUser = {} + // There are two scenarios when using the API: + // 1. A logged in user is accessing protected information + // 2. A user is accessing public information + + // If the user is logged in, we want to pass their infomation + // to the context to be used in the resolvers. + + // If the user is not logged in, we want to pass an empty object + // to the context to be used in the resolvers. + + // We can use the authMiddleware to check for the JWT const res = await authMiddleware(req) - // If JWT exists, decode it and get the user ID - const user = res.headers.get('Authorization') - let userId = '' - if (user) { - userId = jose.decodeJwt(user)['CN'] as string + // If JWT is valid, decode it and get the user ID + if (res.status === 200) { + const token = res.headers.get('Authorization') + + if (token) { + user = { userId: jose.decodeJwt(token)['CN'] as string } + } } + // At this point, we either have an empty user object, or + // a user object with a userId property. + try { const client = await clientConnection() const mongodb = client.db(process.env.MONGODB_DB) + + // If we have a user object with a userId, we want to check if the + // user exists in the portal db. If not, we want to create a new user. + if (user.userId) { + const foundUser = await User.findOne(user.userId, { db: mongodb }) + if (!foundUser) { + // Authenticated user not found in portal db + // Create new record for them + try { + // Create new user in portal db + const initCollection = await getExampleCollection() + + await User.createOne( + user.userId, + [initCollection], + // Default display name is the user's CN since we do not + // have their first and last name on the token. + // If/when they log in to the portal, they can update + user.userId, + 'light', + { + db: mongodb, + } + ) + } catch (e) { + console.error('Error creating user', e) + throw new GraphQLError('Error creating user', { + extensions: { code: 'INTERNAL_SERVER_ERROR' }, + }) + } + } + } + return { // This server utilizes dataSources to access external APIs, similar to the portal // GraphQL server. We're using existing Keystone API data source to avoid duplication. @@ -49,7 +124,7 @@ export default startServerAndCreateNextHandler(server, { keystoneAPI: new ThirdPartyKeystoneAPI({ cache }), mongodb, }, - userId, + user, } } catch (e) { console.error('Error creating GraphQL context', e) diff --git a/src/pages/api/thirdparty/resolvers.test.ts b/src/pages/api/thirdparty/resolvers.test.ts index 967c931a7..416f7e34a 100644 --- a/src/pages/api/thirdparty/resolvers.test.ts +++ b/src/pages/api/thirdparty/resolvers.test.ts @@ -115,6 +115,7 @@ describe('GraphQL resolvers', () => { keystoneAPI, mongodb: db, }, + user: {}, } }) describe('Query.cNotes', () => { @@ -180,7 +181,9 @@ describe('GraphQL resolvers', () => { { contextValue: { ...serverContext, - userId: newPortalUser.userId, + user: { + userId: newPortalUser.userId, + }, }, } )) as SingleGraphQLResponse @@ -246,7 +249,9 @@ describe('GraphQL resolvers', () => { { contextValue: { ...serverContext, - userId: newPortalUser.userId, + user: { + userId: newPortalUser.userId, + }, }, } )) as SingleGraphQLResponse @@ -301,7 +306,9 @@ describe('GraphQL resolvers', () => { { contextValue: { ...serverContext, - userId: newPortalUser.userId, + user: { + userId: newPortalUser.userId, + }, }, } )) as SingleGraphQLResponse @@ -356,7 +363,9 @@ describe('GraphQL resolvers', () => { { contextValue: { ...serverContext, - userId: newPortalUser.userId, + user: { + userId: newPortalUser.userId, + }, }, } )) as SingleGraphQLResponse diff --git a/src/pages/api/thirdparty/resolvers.ts b/src/pages/api/thirdparty/resolvers.ts index 710790017..c820e6274 100644 --- a/src/pages/api/thirdparty/resolvers.ts +++ b/src/pages/api/thirdparty/resolvers.ts @@ -1,15 +1,17 @@ import { DateTime } from 'luxon' import { GraphQLJSON } from 'graphql-type-json' import type { MongoClient } from 'mongodb' +import type { ThirdPartyUser } from './graphql' import ThirdPartyKeystoneAPI from './dataSources/thirdPartyKeystone' import UserModel from 'models/User' + /* Resolver Types */ export type ThirdPartyContext = { dataSources: { keystoneAPI: ThirdPartyKeystoneAPI mongodb: typeof MongoClient } - userId?: string + user: ThirdPartyUser } /* Resolvers */ @@ -36,22 +38,22 @@ export const resolvers = { __: undefined, // We need to alias mongodb as db so we can // use our existing User model - { dataSources: { mongodb: db }, userId }: ThirdPartyContext + { dataSources: { mongodb: db }, user }: ThirdPartyContext ) => { // Make sure we have a userId - if (!userId) { + if (!user.userId) { throw new Error('User not authenticated') } // Look up user in MongoDB and get their display name - return UserModel.getDisplayName(userId, { db }) + return UserModel.getDisplayName(user.userId, { db }) }, documents: async ( _: undefined, __: undefined, - { dataSources: { keystoneAPI }, userId }: ThirdPartyContext + { dataSources: { keystoneAPI }, user }: ThirdPartyContext ) => { - if (!userId) { + if (!user.userId) { throw new Error('User not authenticated') } @@ -65,9 +67,9 @@ export const resolvers = { newsArticles: async ( _: undefined, __: undefined, - { dataSources: { keystoneAPI }, userId }: ThirdPartyContext + { dataSources: { keystoneAPI }, user }: ThirdPartyContext ) => { - if (!userId) { + if (!user.userId) { throw new Error('User not authenticated') } @@ -81,9 +83,9 @@ export const resolvers = { landingPageArticles: async ( _: undefined, __: undefined, - { dataSources: { keystoneAPI }, userId }: ThirdPartyContext + { dataSources: { keystoneAPI }, user }: ThirdPartyContext ) => { - if (!userId) { + if (!user.userId) { throw new Error('User not authenticated') }