Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reauthenticate method to useAuth #721

Merged
merged 4 commits into from
Jun 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,13 @@ The following values are available from the `useAuth` hook:
* async `logIn()`: Differs based on the client library, with Netlify Identity a pop-up is shown, and with Auth0 the user is redirected
* async `logOut()`: Log out the current user
* `currentUser`: an object containing information about the current user, or `null` if the user is not authenticated
* async `reauthenticate()`: Refetch the authentication data and populate the state.
* async `getToken()`: returns a jwt
* `client`: Access the instance of the client which you passed into `AuthProvider`
* `isAuthenticated`: used to determine if the current user has authenticated
* `loading`: The auth state is restored asynchronously when the user visits the site for the first time, use this to determine if you have the correct state


## Usage in Redwood

Redwood provides a zeroconf experience when using our Auth package!
Expand Down
19 changes: 12 additions & 7 deletions packages/auth/src/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@ import { createAuthClient } from './authClients'
export interface CurrentUser {}

export interface AuthContextInterface {
/** Determining your current authentication state */
/* Determining your current authentication state */
loading: boolean
isAuthenticated: boolean
/** The current user data from the `getCurrentUser` function on the api side */
/* The current user data from the `getCurrentUser` function on the api side */
currentUser: null | CurrentUser
/** The user's metadata from the auth provider */
/* The user's metadata from the auth provider */
userMetadata: null | SupportedUserMetadata
logIn(): Promise<void>
logOut(): Promise<void>
getToken(): Promise<null | string>
/** Get the current user from the `getCurrentUser` function on the api side */
/* Fetches the "currentUser" from the api side, but does not update the current user state. */
getCurrentUser(): Promise<null | CurrentUser>
/* Redetermine the users authentication state and update the state. */
reauthenticate(): Promise<void>
/* A reference to the client that you originall passed into the `AuthProvider` during initialization. */
client: SupportedAuthClients
type: SupportedAuthTypes
}
Expand Down Expand Up @@ -53,6 +56,7 @@ type AuthProviderState = {
* </AuthProvider>
* ```
*/
// TODO: Determine what should be done when fetching the current user fails
export class AuthProvider extends React.Component<
AuthProviderProps,
AuthProviderState
Expand All @@ -77,7 +81,7 @@ export class AuthProvider extends React.Component<

async componentDidMount() {
await this.rwClient.restoreAuthState?.()
return this.setAuthState()
return this.reauthenticate()
}

getCurrentUser = async () => {
Expand Down Expand Up @@ -107,7 +111,7 @@ export class AuthProvider extends React.Component<
}
}

setAuthState = async () => {
reauthenticate = async () => {
const userMetadata = await this.rwClient.getUserMetadata()
const isAuthenticated = userMetadata !== null

Expand All @@ -126,7 +130,7 @@ export class AuthProvider extends React.Component<

logIn = async (options?: any) => {
await this.rwClient.login(options)
return this.setAuthState()
return this.reauthenticate()
}

logOut = async (options?: any) => {
Expand All @@ -149,6 +153,7 @@ export class AuthProvider extends React.Component<
logOut: this.logOut,
getToken: this.rwClient.getToken,
getCurrentUser: this.getCurrentUser,
reauthenticate: this.reauthenticate,
client,
type,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
import { setupServer } from 'msw/node'
import { graphql } from 'msw'
import type { AuthClient } from 'src/authClients'
import { AuthProvider } from 'src/AuthProvider'
import { useAuth } from 'src/useAuth'

import type { AuthClient } from './authClients'
import { AuthProvider } from './AuthProvider'
import { useAuth } from './useAuth'
let CURRENT_USER_DATA = {
name: 'Peter Pistorius',
email: 'nospam@example.net',
}

window.__REDWOOD__API_PROXY_PATH = '/.netlify/functions'
const server = setupServer(
graphql.query('__REDWOOD__AUTH_GET_CURRENT_USER', (_req, res, ctx) => {
return res(
ctx.data({
redwood: {
currentUser: {
name: 'Peter Pistorius',
email: 'nospam@example.net',
},
currentUser: CURRENT_USER_DATA,
},
})
)
Expand All @@ -27,6 +28,13 @@ const server = setupServer(
beforeAll(() => server.listen())
afterAll(() => server.close())

beforeEach(() => {
CURRENT_USER_DATA = {
name: 'Peter Pistorius',
email: 'nospam@example.net',
}
})

const AuthConsumer = () => {
const {
loading,
Expand All @@ -35,6 +43,7 @@ const AuthConsumer = () => {
logIn,
userMetadata,
currentUser,
reauthenticate,
} = useAuth()

if (loading) {
Expand All @@ -54,7 +63,13 @@ const AuthConsumer = () => {
{isAuthenticated && (
<>
<p>userMetadata: {JSON.stringify(userMetadata)}</p>
<p>currentUser: {JSON.stringify(currentUser)}</p>
<p>
currentUser:{' '}
{(currentUser && JSON.stringify(currentUser)) ||
'no current user data'}
</p>

<button onClick={() => reauthenticate()}>Update auth data</button>
</>
)}
</>
Expand Down Expand Up @@ -157,14 +172,65 @@ test('Fetching the current user can be skipped', async () => {
// Check that you're logged in!
await waitFor(() => screen.getByText('Log Out'))
expect(mockAuthClient.getUserMetadata).toBeCalledTimes(1)
expect(screen.getByText(/no current user data/)).toBeInTheDocument()

// Log out
fireEvent.click(screen.getByText('Log Out'))
await waitFor(() => screen.getByText('Log In'))
})

/**
* This is especially helpful if you want to update the currentUser state.
*/
test('A user can be reauthenticated to update the "auth state"', async () => {
const mockAuthClient: AuthClient = {
login: async () => {
return true
},
logout: async () => {},
getToken: async () => 'hunter2',
getUserMetadata: jest.fn(async () => {
return null
}),
client: () => {},
type: 'custom',
}
render(
<AuthProvider client={mockAuthClient} type="custom">
<AuthConsumer />
</AuthProvider>
)

// The user is not authenticated
await waitFor(() => screen.getByText('Log In'))
expect(mockAuthClient.getUserMetadata).toBeCalledTimes(1)

// Replace "getUserMetadata" with actual data, and login!
mockAuthClient.getUserMetadata = jest.fn(async () => {
return {
sub: 'abcdefg|123456',
username: 'peterp',
}
})
fireEvent.click(screen.getByText('Log In'))

// Check that you're logged in!
await waitFor(() => screen.getByText('Log Out'))
expect(mockAuthClient.getUserMetadata).toBeCalledTimes(1)

// The original current user data is fetched.
expect(
screen.getByText(
'userMetadata: {"sub":"abcdefg|123456","username":"peterp"}'
'currentUser: {"name":"Peter Pistorius","email":"nospam@example.net"}'
)
).toBeInTheDocument()
expect(screen.getByText('currentUser:')).toBeInTheDocument()

// Log out
fireEvent.click(screen.getByText('Log Out'))
await waitFor(() => screen.getByText('Log In'))
// Sometime changes over...
CURRENT_USER_DATA.name = 'Rambo'
fireEvent.click(screen.getByText('Update auth data'))
waitFor(() =>
screen.getByText(
'currentUser: {"name":"Rambo","email":"nospam@example.net"}'
)
)
})