redwoodjs / redwoodjs-com-archive

Public website for RedwoodJS
https://redwoodjs.com
129 stars 156 forks source link

Cookbook: user creation on signup with 3rd party service #212

Open cannikin opened 4 years ago

cannikin commented 4 years ago

Show how to create a user in your local DB as the result of a user signing up on a third party service. Simplest example would be with Netlify Identity and creating the user if they don't exist when you try to find them in api/src/lib/auth.js#getCurrentUser but the exact same instructions would work with any auth provider that returns an email address in their JWT response.

Netlify Identity also supports webhooks when a new user signs up, we could include this as an alternate method of user creation (and as a bonus also shows people how to create a custom function that uses existing services).

image

See https://github.com/redwoodjs/redwood/issues/745 and https://community.redwoodjs.com/t/auth-what-about-signing-up/796/2

dthyresson commented 4 years ago

This is what I have been doing with Auth0 (which could also be setup to do hooks on signup/signin etc ... in fact).

That said: the webhook approach is actually a much better implementation since it would not clutter getCurrentUser with something that only happens once it a while ie, sign up ... and a webhook on sign in would keep the User record up-to-date.

import { AuthenticationClient } from 'auth0'
import { AuthenticationError } from '@redwoodjs/api'
import { context } from '@redwoodjs/api/dist/globalContext'

import { db } from 'src/lib/db'

const auth0 = new AuthenticationClient({
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
})

export const getCurrentUser = async (decoded, { authType, token }) => {
  // shortcut if we have a user profile in context
  // note: if something else changes the User record
  // can't rely on this shortcut but will reduce db calls
  if (context.currentUser?.userId) {
    return context.currentUser
  }

  // do we have an accessToken from auth0 and an userId from the decoded JWT?
  if (!token || authType != 'auth0' || !decoded?.sub) {
    return decoded
  }

  // find the existing User ...
  // or create a new User with its userProfile info
  try {
    const user = await db.user.findOne({
      where: {
        userId: decoded.sub,
      },
    })

    if (!user && token) {
      const auth0User = await auth0.getProfile(token)
      const userProfile = {
        email: auth0User.email,
        emailVerified: auth0User.emailVerified,
        lastIp: auth0User.lastIp,
        lastLogin: auth0User.lastLogin,
        loginsCount: auth0User.loginsCount,
        name: auth0User.name,
        nickname: auth0User.nickname,
        picture: auth0User.picture,
        userId: auth0User.sub,
      }

      const userWithProfile = await db.user.create({
        data: userProfile,
      })

      // set the currentUser in context to include its userProfile info
      const currentUser = context.currentUser
      context.currentUser = { currentUser, ...userProfile }

      return userWithProfile
    }

    return user
  } catch (error) {
    console.log(error)
    return decoded
  }
}

with a User model:

model User {
  id            Int      @id @default(autoincrement())
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
  email         String   @unique
  emailVerified Boolean?
  lastIp        String?
  lastLogin     DateTime @default(now())
  loginsCount   Int      @default(0)
  name          String
  nickname      String
  picture       String
  userId        String   @unique
}

and users service:

import { db } from 'src/lib/db'

export const users = () => {
  return db.user.findMany()
}

export const user = ({ id }) => {
  return db.user.findOne({
    where: { id },
  })
}

export const userByUserId = ({ userId }) => {
  return db.user.findOne({
    where: { userId },
  })
}

export const createUser = ({ input }) => {
  return db.user.create({
    data: input,
  })
}
...