Skip to content

Commit

Permalink
Merge branch 'main' of github.com:redwoodjs/redwood into feat/dc-rc-o…
Browse files Browse the repository at this point in the history
…g-gen-mw-p2

* 'main' of github.com:redwoodjs/redwood:
  feat(server-auth): WebAuthN support during SSR (#10498)
  Removes old HTML comments from CLI commands doc
  feat(cookieJar): Change cookie.get to directly return value (#10493)
  • Loading branch information
dac09 committed Apr 23, 2024
2 parents f534c6a + 3f57976 commit b857fe3
Show file tree
Hide file tree
Showing 16 changed files with 81 additions and 68 deletions.
19 changes: 19 additions & 0 deletions .changesets/10493.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
- feat(cookieJar): Change cookie.get to directly return value (#10493) by @dac09

**Motivation**
My original design of the `CookeiJar.get` would return the full cookie object we store, including cookie options. This is not ideal because you need to access the cookie like this:

```js
const myCookie = mwRequest.cookies.get('myCookie')

// 👇
const actualValue = myCookie.value
```

This is unwieldy, and feels unergonomic for the 98% of cases where `get` will be used to just see the value.

**How do I still see the options of the cookie?**
You can still access all the details of the cookie by doing `cookie.entries`. I don't really have a case for this yet, so let's not optimise for this case, but we know it's possible!


This is me just stabilizing the API for Middleware stuff, before we ship it out of experimental
10 changes: 10 additions & 0 deletions .changesets/10498.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- feat(server-auth): WebAuthN support during SSR (#10498) by @dac09

**This PR changes the following:**
**1. Moves webAuthN imports to be dynamic imports**
This is because the dbauth-provider-web packages are still CJS only. When importing in an ESM environment (such as SSR/RSC server) - it complains that about ESM imports

**2. Updates the default auth provider state for middleware auth**
Middleware auth default state is _almost_ the same as SPA default auth state. Except that loading is always false! Otherwise you can get stuck in a loading state forever.


23 changes: 0 additions & 23 deletions docs/docs/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1193,9 +1193,6 @@ yarn redwood generate sdl <model>
The sdl will inspect your `schema.prisma` and will do its best with relations. Schema to generators isn't one-to-one yet (and might never be).
<!-- See limited generator support for relations
https://community.redwoodjs.com/t/prisma-beta-2-and-redwoodjs-limited-generator-support-for-relations-with-workarounds/361 -->
| Arguments & Options | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `model` | Model to generate the sdl for |
Expand Down Expand Up @@ -1627,16 +1624,6 @@ If you wanted to seed your database using a different method (like `psql` and an
In addition, you can [code along with Ryan Chenkie](https://www.youtube.com/watch?v=2LwTUIqjbPo), and learn how libraries like [faker](https://www.npmjs.com/package/faker) can help you create a large, realistic database fast, especially in tandem with Prisma's [createMany](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#createmany).
<!-- ### generate -->
<!-- Generate artifacts (e.g. Prisma Client). -->
<!-- > 👉 Quick link to the [Prisma CLI Reference](https://www.prisma.io/docs/reference/api-reference/command-reference#generate). -->
<!-- ``` -->
<!-- yarn redwood prisma generate -->
<!-- ``` -->
**Log Formatting**
If you use the Redwood Logger as part of your seed script, you can pipe the command to the LogFormatter to output prettified logs.
Expand Down Expand Up @@ -1714,16 +1701,6 @@ Create a migration from changes in Prisma schema, apply it to the database, trig
yarn redwood prisma migrate dev
```
<!-- #### reset -->
<!-- Reset your database and apply all migrations, all data will be lost. -->
<!-- > 👉 Quick link to the [Prisma CLI Reference](https://www.prisma.io/docs/reference/api-reference/command-reference#migrate-reset). -->
<!-- ``` -->
<!-- yarn redwood prisma migrate reset -->
<!-- ``` -->
#### prisma migrate deploy
Apply pending migrations to update the database schema in production/staging.
Expand Down
14 changes: 8 additions & 6 deletions packages/auth-providers/dbAuth/web/src/webAuthn.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import {
startRegistration,
startAuthentication,
browserSupportsWebAuthn,
} from '@simplewebauthn/browser'

class WebAuthnRegistrationError extends Error {
constructor(message: string) {
super(message)
Expand Down Expand Up @@ -55,10 +49,15 @@ export default class WebAuthnClient {
}

async isSupported() {
const { browserSupportsWebAuthn } = await import('@simplewebauthn/browser')
return await browserSupportsWebAuthn()
}

isEnabled() {
if (typeof window === 'undefined') {
return false
}

return !!/\bwebAuthn\b/.test(document.cookie)
}

Expand Down Expand Up @@ -99,6 +98,7 @@ export default class WebAuthnClient {

async authenticate() {
const authOptions = await this.authenticationOptions()
const { startAuthentication } = await import('@simplewebauthn/browser')

try {
const browserResponse = await startAuthentication(authOptions)
Expand Down Expand Up @@ -173,6 +173,8 @@ export default class WebAuthnClient {
const options = await this.registrationOptions()
let regResponse

const { startRegistration } = await import('@simplewebauthn/browser')

try {
regResponse = await startRegistration(options)
} catch (e: any) {
Expand Down
4 changes: 2 additions & 2 deletions packages/auth/src/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { AuthContextInterface, CurrentUser } from '../AuthContext.js'
import type { AuthImplementation } from '../AuthImplementation.js'

import type { AuthProviderState } from './AuthProviderState.js'
import { defaultAuthProviderState } from './AuthProviderState.js'
import { spaDefaultAuthProviderState } from './AuthProviderState.js'
import { ServerAuthContext } from './ServerAuthProvider.js'
import { useCurrentUser } from './useCurrentUser.js'
import { useForgotPassword } from './useForgotPassword.js'
Expand Down Expand Up @@ -83,7 +83,7 @@ export function createAuthProvider<

const [authProviderState, setAuthProviderState] = useState<
AuthProviderState<TUser>
>(serverAuthState || defaultAuthProviderState)
>(serverAuthState || spaDefaultAuthProviderState)

const getToken = useToken(authImplementation)

Expand Down
10 changes: 9 additions & 1 deletion packages/auth/src/AuthProvider/AuthProviderState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ export type AuthProviderState<TUser, TClient = unknown> = {
client?: TClient
}

export const defaultAuthProviderState: AuthProviderState<never> = {
export const spaDefaultAuthProviderState: AuthProviderState<never> = {
loading: true,
isAuthenticated: false,
userMetadata: null,
currentUser: null,
hasError: false,
}

export const middlewareDefaultAuthProviderState: AuthProviderState<never> = {
loading: false,
isAuthenticated: false,
userMetadata: null,
currentUser: null,
hasError: false,
}
6 changes: 3 additions & 3 deletions packages/auth/src/AuthProvider/ServerAuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactNode } from 'react'
import React from 'react'

import type { AuthProviderState } from './AuthProviderState.js'
import { defaultAuthProviderState } from './AuthProviderState.js'
import { middlewareDefaultAuthProviderState } from './AuthProviderState.js'

export type ServerAuthState = AuthProviderState<never> & {
cookieHeader?: string
Expand All @@ -11,7 +11,7 @@ export type ServerAuthState = AuthProviderState<never> & {
const getAuthInitialStateFromServer = () => {
if (globalThis?.__REDWOOD__SERVER__AUTH_STATE__) {
const initialState = {
...defaultAuthProviderState,
...middlewareDefaultAuthProviderState,
encryptedSession: null,
...(globalThis?.__REDWOOD__SERVER__AUTH_STATE__ || {}),
}
Expand All @@ -25,7 +25,7 @@ const getAuthInitialStateFromServer = () => {
}

/**
* On the server, it resolves to the defaultAuthProviderState first.
* On the server, it resolves to the middlewareDefaultAuthProviderState first.
*
* On the client it restores from the initial server state injected in the ServerAuthProvider
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/auth/src/AuthProvider/useLogIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useCallback } from 'react'
import type { AuthImplementation } from '../AuthImplementation.js'

import type { AuthProviderState } from './AuthProviderState.js'
import { defaultAuthProviderState } from './AuthProviderState.js'
import { spaDefaultAuthProviderState } from './AuthProviderState.js'
import type { useCurrentUser } from './useCurrentUser.js'
import { useReauthenticate } from './useReauthenticate.js'

Expand Down Expand Up @@ -50,7 +50,7 @@ export const useLogIn = <

return useCallback(
async (options?: TLogInOptions) => {
setAuthProviderState(defaultAuthProviderState)
setAuthProviderState(spaDefaultAuthProviderState)
const loginResult = await authImplementation.login(options)
await reauthenticate()

Expand Down
18 changes: 6 additions & 12 deletions packages/vite/src/middleware/CookieJar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,17 @@ describe('CookieJar', () => {
)

test('instatitates cookie jar from a cookie string', () => {
expect(cookieJar.get('color_mode')).toStrictEqual({
value: JSON.stringify({
expect(cookieJar.get('color_mode')).toStrictEqual(
JSON.stringify({
color_mode: 'light',
light_theme: { name: 'light', color_mode: 'light' },
dark_theme: { name: 'dark_dimmed', color_mode: 'dark' },
}),
})
)

expect(cookieJar.get('preferred_color_mode')).toStrictEqual({
value: 'dark',
})
expect(cookieJar.get('preferred_color_mode')).toStrictEqual('dark')

expect(cookieJar.get('tz')).toStrictEqual({
value: 'Asia/Bangkok',
})
expect(cookieJar.get('tz')).toStrictEqual('Asia/Bangkok')
})

describe('Helper methods like JS Map', () => {
Expand Down Expand Up @@ -60,11 +56,9 @@ describe('CookieJar', () => {

myJar.unset('auth_provider')

const { value: authProviderValue, options: authProviderOptions } =
myJar.get('auth_provider')!
const authProviderValue = myJar.get('auth_provider')

expect(authProviderValue).toBeFalsy()
expect(authProviderOptions!.expires).toStrictEqual(new Date(0))
})

test('clear All', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/middleware/CookieJar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class CookieJar {
}

public get(name: string) {
return this.map.get(name)
return this.map.get(name)?.value
}

public has(name: string) {
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/middleware/MiddlewareRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('MiddlewareRequest', () => {
})
const mReq = createMiddlewareRequest(req)

expect(mReq.cookies.get('foo')).toStrictEqual({ value: 'bar' })
expect(mReq.cookies.get('foo')).toStrictEqual('bar')
expect(mReq.method).toStrictEqual('POST')
expect(mReq.headers.get('Content-Type')).toStrictEqual('application/json')

Expand All @@ -43,7 +43,7 @@ describe('MiddlewareRequest', () => {

const mReq = createMiddlewareRequest(whatWgRequest)

expect(mReq.cookies.get('errybody')).toStrictEqual({ value: 'lets-funk' })
expect(mReq.cookies.get('errybody')).toStrictEqual('lets-funk')
expect(mReq.method).toStrictEqual('PUT')

expect(mReq.headers.get('x-custom-header')).toStrictEqual('beatdrop')
Expand Down
7 changes: 5 additions & 2 deletions packages/vite/src/middleware/MiddlewareRequest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Request as WhatWgRequest } from '@whatwg-node/fetch'

import { defaultAuthProviderState, type ServerAuthState } from '@redwoodjs/auth'
import {
middlewareDefaultAuthProviderState,
type ServerAuthState,
} from '@redwoodjs/auth'

import { CookieJar } from './CookieJar.js'

Expand All @@ -27,7 +30,7 @@ export class MiddlewareRequest extends WhatWgRequest {
constructor(input: Request) {
super(input)
this.cookies = new CookieJar(input.headers.get('Cookie'))
this.serverAuthContext = new ContextJar(defaultAuthProviderState)
this.serverAuthContext = new ContextJar(middlewareDefaultAuthProviderState)
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/vite/src/middleware/invokeMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MockInstance } from 'vitest'
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'

import { defaultAuthProviderState } from '@redwoodjs/auth'
import { middlewareDefaultAuthProviderState } from '@redwoodjs/auth'

import { invoke } from './invokeMiddleware'
import type { MiddlewareRequest } from './MiddlewareRequest'
Expand All @@ -11,7 +11,7 @@ describe('Invoke middleware', () => {
test('returns a MiddlewareResponse, even if no middleware defined', async () => {
const [mwRes, authState] = await invoke(new Request('https://example.com'))
expect(mwRes).toBeInstanceOf(MiddlewareResponse)
expect(authState).toEqual(defaultAuthProviderState)
expect(authState).toEqual(middlewareDefaultAuthProviderState)
})

test('extracts auth state correctly, and always returns a MWResponse', async () => {
Expand Down Expand Up @@ -55,7 +55,7 @@ describe('Invoke middleware', () => {
)

expect(mwRes).toBeInstanceOf(MiddlewareResponse)
expect(authState).toEqual(defaultAuthProviderState)
expect(authState).toEqual(middlewareDefaultAuthProviderState)
})
})
})
7 changes: 5 additions & 2 deletions packages/vite/src/middleware/invokeMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { defaultAuthProviderState, type ServerAuthState } from '@redwoodjs/auth'
import {
middlewareDefaultAuthProviderState,
type ServerAuthState,
} from '@redwoodjs/auth'

import { MiddlewareRequest } from './MiddlewareRequest.js'
import { MiddlewareResponse } from './MiddlewareResponse.js'
Expand All @@ -18,7 +21,7 @@ export const invoke = async (
options?: MiddlewareInvokeOptions,
): Promise<[MiddlewareResponse, ServerAuthState]> => {
if (typeof middleware !== 'function') {
return [MiddlewareResponse.next(), defaultAuthProviderState]
return [MiddlewareResponse.next(), middlewareDefaultAuthProviderState]
}

const mwReq = new MiddlewareRequest(req)
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/middleware/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ describe('chain', () => {
expect(output.headers.get('class-mw-value')).toBe('999')

// The other one still gets chained
expect(output.cookies.get('add-cookie-mw').value).toBe('added')
expect(output.cookies.get('add-cookie-mw')).toBe('added')

// Because /bazinga is more specific, the '*' handlers won't be executed
expect(output.headers.get('add-header-mw')).toBeFalsy()
Expand Down
13 changes: 5 additions & 8 deletions packages/vite/src/streaming/createReactStreamingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { HTTPMethod } from 'find-my-way'
import isbot from 'isbot'
import type { ViteDevServer } from 'vite'

import { defaultAuthProviderState } from '@redwoodjs/auth'
import { middlewareDefaultAuthProviderState } from '@redwoodjs/auth'
import type { RouteSpec, RWRouteManifestItem } from '@redwoodjs/internal'
import { getAppRouteHook, getConfig, getPaths } from '@redwoodjs/project-config'
import { matchPath } from '@redwoodjs/router'
Expand Down Expand Up @@ -69,7 +69,7 @@ export const createReactStreamingHandler = async (
// @NOTE: we are returning a FetchAPI handler
return async (req: Request) => {
let mwResponse = MiddlewareResponse.next()
let decodedAuthState = defaultAuthProviderState
let decodedAuthState = middlewareDefaultAuthProviderState
// @TODO: Make the currentRoute 404?
let currentRoute: RWRouteManifestItem | undefined
let parsedParams: any = {}
Expand All @@ -92,16 +92,13 @@ export const createReactStreamingHandler = async (
// ~~~ Middleware Handling ~~~
if (middlewareRouter) {
const matchedMw = middlewareRouter.find(req.method as HTTPMethod, req.url)
;[mwResponse, decodedAuthState = defaultAuthProviderState] = await invoke(
req,
matchedMw?.handler as Middleware | undefined,
{
;[mwResponse, decodedAuthState = middlewareDefaultAuthProviderState] =
await invoke(req, matchedMw?.handler as Middleware | undefined, {
route: currentRoute,
cssPaths: getStylesheetLinks(currentRoute),
params: matchedMw?.params,
viteDevServer,
},
)
})

// If mwResponse is a redirect, short-circuit here, and skip React rendering
// If the response has a body, no need to render react.
Expand Down

0 comments on commit b857fe3

Please sign in to comment.