Skip to content

Commit

Permalink
feat: [Auth] Common AuthProvider & use* changes for middleware auth (#…
Browse files Browse the repository at this point in the history
…10420)

Pairing with @dthyresson

- First step of supporting Auth using middleware. This PR is the
smallest slice possible.
- Also removes the unused `skipFetchCurrentUser`, that was only used in
`nhost` - a deprecated auth provider. This technically makes this change
breaking, so we need to think it through!

To make use of this functionality, we will need:
**1. A specialised middleware auth client**
(naming TBC) - in your `web/src/auth.ts`, you will need to instantiate
the create auth differently.

```
import {
  createDbAuthClient,
  createMiddlewareAuth, // 👈 not in this PR
} from '@redwoodjs/auth-dbauth-web'

const dbAuthClient = createDbAuthClient({
  middleware: true, // 👈 not in this PR
})

export const { AuthProvider, useAuth } = createMiddlewareAuth(dbAuthClient)
```

2. The middleware to perform auth
```
export const registerMiddleware = () => {
  console.log('Registering middleware')
  const dbAuthMiddleware = createDbAuthMiddleware({
    cookieName,
  })
  return [dbAuthMiddleware]
}
```
4. Updated dbAuth handler for dbAuth

**Checklist**

- [x] Ensure backwards compatibility with non-SSR auth
- [x] Should we undo the changes to `skipFetchCurrentUser`

---------

Co-authored-by: David Thyresson <dthyresson@gmail.com>
  • Loading branch information
dac09 and dthyresson authored Apr 10, 2024
1 parent 07361ec commit 3252de7
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 65 deletions.
12 changes: 12 additions & 0 deletions .changesets/10420.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- feat: [Auth] Common AuthProvider & use* changes for middleware auth #10420 by @dac09 and @dthyresson

* First step of supporting Auth using middleware
* Ensure backwards compatibility with non-SSR auth

### Breaking Change

Removes `skipFetchCurrentUser` which was used by the no longer existing nHost auth provider, but could potentially have been used by custom auth.




29 changes: 13 additions & 16 deletions packages/auth/src/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { useToken } from './useToken.js'
import { useValidateResetToken } from './useValidateResetToken.js'

export interface AuthProviderProps {
skipFetchCurrentUser?: boolean
children: ReactNode
}

Expand Down Expand Up @@ -77,10 +76,7 @@ export function createAuthProvider<
) => (rolesToCheck: string | string[]) => boolean
},
) {
const AuthProvider = ({
children,
skipFetchCurrentUser,
}: AuthProviderProps) => {
const AuthProvider = ({ children }: AuthProviderProps) => {
// const [hasRestoredState, setHasRestoredState] = useState(false)

const serverAuthState = useContext(ServerAuthContext)
Expand All @@ -104,7 +100,6 @@ export function createAuthProvider<
authImplementation,
setAuthProviderState,
getCurrentUser,
skipFetchCurrentUser,
)

const hasRole = customProviderHooks?.useHasRole
Expand All @@ -115,13 +110,11 @@ export function createAuthProvider<
authImplementation,
setAuthProviderState,
getCurrentUser,
skipFetchCurrentUser,
)
const logIn = useLogIn(
authImplementation,
setAuthProviderState,
getCurrentUser,
skipFetchCurrentUser,
)
const logOut = useLogOut(authImplementation, setAuthProviderState)
const forgotPassword = useForgotPassword(authImplementation)
Expand All @@ -130,18 +123,22 @@ export function createAuthProvider<
const type = authImplementation.type
const client = authImplementation.client

// Whenever the authImplementation is ready to go, restore auth and
// reauthenticate
// Whenever the authImplementation is ready to go, restore auth and reauthenticate
useEffect(() => {
async function doRestoreState() {
// @MARK: this is where we fetch currentUser from graphql again
// because without SSR, initial state doesn't exist
// what we want to do here is to conditionally call reauthenticate
// so that the restoreAuthState comes from the injected state

await authImplementation.restoreAuthState?.()

// @MARK(SSR-Auth): Conditionally call reauthenticate, because initial
// state should come from server (on SSR).
// If the initial state didn't come from the server - or was restored
// already - reauthenticate will make a call to receive the current
// user from the server
reauthenticate()
// If the inital state didn't come from the server (or was restored before)
// reauthenticate will make an API call to the middleware to receive the current user
// (instead of called the graphql endpoint with currentUser)
if (!serverAuthState) {
reauthenticate()
}
}

doRestoreState()
Expand Down
2 changes: 0 additions & 2 deletions packages/auth/src/AuthProvider/useLogIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,11 @@ export const useLogIn = <
React.SetStateAction<AuthProviderState<TUser>>
>,
getCurrentUser: ReturnType<typeof useCurrentUser>,
skipFetchCurrentUser: boolean | undefined,
) => {
const reauthenticate = useReauthenticate(
authImplementation,
setAuthProviderState,
getCurrentUser,
skipFetchCurrentUser,
)

return useCallback(
Expand Down
16 changes: 5 additions & 11 deletions packages/auth/src/AuthProvider/useReauthenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export const useReauthenticate = <TUser>(
React.SetStateAction<AuthProviderState<TUser>>
>,
getCurrentUser: ReturnType<typeof useCurrentUser>,
skipFetchCurrentUser: boolean | undefined,
) => {
const getToken = useToken(authImplementation)

Expand Down Expand Up @@ -53,15 +52,16 @@ export const useReauthenticate = <TUser>(
client: authImplementation.client,
})
} else {
// This call here is a local check against the auth provider's client.
// e.g. if the auth sdk has logged you out, it'll throw an error
await getToken()

const currentUser = skipFetchCurrentUser ? null : await getCurrentUser()
const currentUser = await getCurrentUser()

setAuthProviderState((oldState) => ({
...oldState,
userMetadata,
currentUser,
isAuthenticated: true,
isAuthenticated: !!currentUser,
loading: false,
client: authImplementation.client,
}))
Expand All @@ -73,11 +73,5 @@ export const useReauthenticate = <TUser>(
error: e as Error,
})
}
}, [
authImplementation,
getToken,
setAuthProviderState,
skipFetchCurrentUser,
getCurrentUser,
])
}, [authImplementation, setAuthProviderState, getToken, getCurrentUser])
}
2 changes: 0 additions & 2 deletions packages/auth/src/AuthProvider/useSignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@ export const useSignUp = <
React.SetStateAction<AuthProviderState<TUser>>
>,
getCurrentUser: ReturnType<typeof useCurrentUser>,
skipFetchCurrentUser: boolean | undefined,
) => {
const reauthenticate = useReauthenticate(
authImplementation,
setAuthProviderState,
getCurrentUser,
skipFetchCurrentUser,
)

return useCallback(
Expand Down
37 changes: 3 additions & 34 deletions packages/auth/src/__tests__/AuthProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,40 +222,9 @@ describe('Custom auth provider', () => {
await waitFor(() => screen.getByText('Log In'))
})

test('Fetching the current user can be skipped', async () => {
const mockAuthClient = customTestAuth

render(
<AuthProvider skipFetchCurrentUser>
<AuthConsumer />
</AuthProvider>,
)

// We're booting up!
expect(screen.getByText('Loading...')).toBeInTheDocument()

// 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 = vi.fn(() => {
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)
expect(screen.getByText(/no current user data/)).toBeInTheDocument()

// Log out
fireEvent.click(screen.getByText('Log Out'))
await waitFor(() => screen.getByText('Log In'))
})
/// @MARK: Technically a breaking change.
// skipFetchCurrentUser used to be used for nHost only
// and isn't something we need to support anymore

/**
* This is especially helpful if you want to update the currentUser state.
Expand Down

0 comments on commit 3252de7

Please sign in to comment.