Skip to content

Commit

Permalink
feat: Create new portal user from JWT request if one doesn't exist (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
abbyoung authored Feb 16, 2024
1 parent 95b49ac commit 7e0102c
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 33 deletions.
2 changes: 1 addition & 1 deletion src/pages/api/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
111 changes: 93 additions & 18 deletions src/pages/api/thirdparty/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,129 @@ 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.
dataSources: {
keystoneAPI: new ThirdPartyKeystoneAPI({ cache }),
mongodb,
},
userId,
user,
}
} catch (e) {
console.error('Error creating GraphQL context', e)
Expand Down
17 changes: 13 additions & 4 deletions src/pages/api/thirdparty/resolvers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ describe('GraphQL resolvers', () => {
keystoneAPI,
mongodb: db,
},
user: {},
}
})
describe('Query.cNotes', () => {
Expand Down Expand Up @@ -180,7 +181,9 @@ describe('GraphQL resolvers', () => {
{
contextValue: {
...serverContext,
userId: newPortalUser.userId,
user: {
userId: newPortalUser.userId,
},
},
}
)) as SingleGraphQLResponse<ResponseData>
Expand Down Expand Up @@ -246,7 +249,9 @@ describe('GraphQL resolvers', () => {
{
contextValue: {
...serverContext,
userId: newPortalUser.userId,
user: {
userId: newPortalUser.userId,
},
},
}
)) as SingleGraphQLResponse<ResponseData>
Expand Down Expand Up @@ -301,7 +306,9 @@ describe('GraphQL resolvers', () => {
{
contextValue: {
...serverContext,
userId: newPortalUser.userId,
user: {
userId: newPortalUser.userId,
},
},
}
)) as SingleGraphQLResponse<ResponseData>
Expand Down Expand Up @@ -356,7 +363,9 @@ describe('GraphQL resolvers', () => {
{
contextValue: {
...serverContext,
userId: newPortalUser.userId,
user: {
userId: newPortalUser.userId,
},
},
}
)) as SingleGraphQLResponse<ResponseData>
Expand Down
22 changes: 12 additions & 10 deletions src/pages/api/thirdparty/resolvers.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -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')
}

Expand All @@ -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')
}

Expand All @@ -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')
}

Expand Down

0 comments on commit 7e0102c

Please sign in to comment.