Skip to content

Commit b54f8d6

Browse files
committed
docs(authentication): document the replaceable react-auth transport
1 parent 54ad965 commit b54f8d6

2 files changed

Lines changed: 162 additions & 44 deletions

File tree

‎src/content/docs/authentication/overview.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,12 @@ per-IP meaningful behind a proxy.
4949

5050
**[`@gopherium/react-auth`](https://www.npmjs.com/package/@gopherium/react-auth)**
5151
is the browser side: a session-aware gate, hooks, typed errors, a
52-
WordPress-Design-System skin, an admin UI, and an msw-based
52+
WordPress Design System skin, an admin UI, and a
5353
[test harness](/authentication/react-integration/#testing-components-that-authenticate).
54+
Its backend calls run through a
55+
[replaceable transport](/authentication/react-integration/#talking-to-a-backend-that-is-not-rest),
56+
so an application serving GraphQL keeps the screens and swaps the
57+
requests underneath them.
5458

5559
## Adopt what you need
5660

‎src/content/docs/authentication/react-integration.md‎

Lines changed: 157 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ description: The session gate, the hooks, the ready-made screens, and the test h
44
---
55

66
[`@gopherium/react-auth`](https://www.npmjs.com/package/@gopherium/react-auth)
7-
is the browser half of the authentication stack, speaking authkit's
8-
JSON dialect. It ships four entry points so you take only the layers
9-
you want:
7+
is the browser half of the authentication stack. It calls the
8+
[authkit endpoints](/authentication/sessions-over-http/) for you and
9+
gives your React application everything around a login.
10+
11+
It ships four entry points, so you take only the layers you want:
1012

1113
| Entry | Contents |
1214
| --- | --- |
13-
| `@gopherium/react-auth` | Headless core: gate, hooks, api client, typed errors |
15+
| `@gopherium/react-auth` | The core: gate, hooks, backend calls, typed errors, transport |
1416
| `@gopherium/react-auth/wpds` | Ready-made screens on the WordPress Design System |
15-
| `@gopherium/react-auth/admin` | The user-administration api client |
16-
| `@gopherium/react-auth/testing` | msw harness, canned handlers, session seeding |
17+
| `@gopherium/react-auth/admin` | The user administration calls |
18+
| `@gopherium/react-auth/testing` | Test harness, canned responses, session seeding |
1719

18-
`react` and `@tanstack/react-query` are peer dependencies. The `/wpds`
19-
screens additionally expect `@wordpress/ui`.
20+
`react` and `@tanstack/react-query` are peer dependencies, meaning
21+
your application installs them. The `/wpds` screens also expect
22+
`@wordpress/ui`.
2023

2124
## The gate
2225

@@ -32,46 +35,147 @@ const queryClient = createAuthQueryClient()
3235
</QueryClientProvider>
3336
```
3437

35-
`AuthGate` resolves the session and renders one of three things: your
36-
app when signed in, the login screen when signed out, or the loading
37-
and error states, both overridable props. The `loginScreen` render
38-
prop receives the login-completion handler, so any login UI plugs in.
38+
`AuthGate` checks whether there is a session and renders one of four
39+
things: your application when signed in, the login screen when signed
40+
out, and a loading or error state in between. The last two have
41+
default UI you can replace with props.
42+
43+
`loginScreen` is a render prop, so you can pass any login UI you
44+
like. It receives a handler to call once the login succeeds.
3945

40-
`createAuthQueryClient` matters as much as the gate: it builds a query
41-
client whose caches drop the session whenever any query or mutation
42-
fails with an `UnauthorizedError`. A session revoked mid-use brings
43-
the login screen back without a reload, from anywhere in the app.
46+
`createAuthQueryClient` is easy to skip past, and it matters as much
47+
as the gate. It builds a React Query client that watches for
48+
`UnauthorizedError` coming back from any query or mutation, and drops
49+
the cached session when it sees one. The effect is that a session
50+
revoked while someone is working brings the login screen straight
51+
back, from anywhere in the application, with no page reload.
4452

4553
## Hooks and the session key
4654

47-
- `useSession()` reads the cached identity.
48-
- `useLogout()` logs out and scrubs every cached query except the
49-
session itself, so no stale data survives into the next login.
50-
- `sessionQueryKey` is owned here, at the bottom of the dependency
51-
graph. Anything else that touches the session, such as an SSE layer
52-
probing whether a dead stream means a revoked login, imports the key
53-
and `isSessionRevoked` from this package, never the other way
54-
around.
55+
- `useSession()` reads the signed-in user from the cache.
56+
- `useLogout()` logs out and clears every cached query except the
57+
session itself, so one user's data cannot show up after the next
58+
person logs in.
59+
- `sessionQueryKey` is the React Query key for the session, and this
60+
package owns it.
61+
62+
That last point matters when other parts of your application need to
63+
care about the session. Say you have a live event stream that dies,
64+
and you want to know whether the cause was a revoked login. That code
65+
imports `sessionQueryKey` and `isSessionRevoked` from this package.
66+
Dependencies always point this way, never outward, so there is
67+
exactly one definition of what the session is.
5568

5669
## Ready-made screens
5770

58-
The `/wpds` entry covers the whole surface for apps on the WordPress
59-
Design System: `LoginScreen` with a `brand` prop, `AccountPanel` for
60-
the signed-in identity and logout control, `UsersScreen` and
61-
`NewUserScreen` for administration, and `usersNavItem` for your
62-
navigation. Router coupling stays out through render props: the users
63-
screen takes a `newUserRender` element for its create link, and the
64-
new-user form reports success through `onCreated` so the app decides
65-
where to navigate. Import the stylesheet once:
71+
If your application uses the WordPress Design System, the `/wpds`
72+
entry has every screen a login needs already built:
73+
74+
- `LoginScreen`, which takes a `brand` prop
75+
- `AccountPanel`, showing who is signed in with a logout control
76+
- `UsersScreen` and `NewUserScreen` for user administration
77+
- `usersNavItem` for your navigation
78+
79+
None of them know your router, and they stay that way by handing
80+
navigation back to you. `UsersScreen` takes `newUserRender`, an
81+
element to use as its create link, so you supply your own link
82+
component. `NewUserScreen` calls `onCreated` when the user is saved,
83+
and your application decides where to go next.
84+
85+
Import the stylesheet once:
6686

6787
```tsx
6888
import '@gopherium/react-auth/wpds/style.css'
6989
```
7090

91+
## Talking to a backend that is not REST
92+
93+
By default the package calls authkit's REST endpoints directly, such
94+
as `POST /api/auth/login`. If your application talks to its backend
95+
some other way, most often GraphQL, you would otherwise be stuck:
96+
the screens and hooks are useful, but the requests they make are
97+
wrong for your server.
98+
99+
`configureAuthTransport` replaces those requests. Call it once when
100+
your application starts, before anything renders:
101+
102+
```ts
103+
import {
104+
InvalidCredentialsError,
105+
configureAuthTransport,
106+
} from '@gopherium/react-auth'
107+
108+
configureAuthTransport({
109+
async login(email, password) {
110+
const result = await graphqlClient.mutation(LoginDocument, { email, password })
111+
if (result.error) {
112+
throw new InvalidCredentialsError('invalid credentials')
113+
}
114+
return result.data.login
115+
},
116+
})
117+
```
118+
119+
Everything above this line keeps working unchanged. The screens, the
120+
gate, the hooks and the query keys never know the requests moved.
121+
122+
There are seven operations in the `AuthTransport` interface:
123+
`fetchSession`, `login`, `logout`, `isSessionRevoked`, `fetchUsers`,
124+
`createUser` and `setUserDisabled`. You override the ones you want
125+
and the rest keep using REST, which makes a gradual migration
126+
possible. Calls to `configureAuthTransport` add up rather than
127+
replace each other, so you can configure in more than one place.
128+
129+
### Matching the error contract
130+
131+
This is the part that is easy to get wrong. The hooks and screens
132+
decide what to show by catching specific error classes, so your
133+
implementation has to throw the same ones the REST version throws.
134+
A generic `Error` where the package expects `InvalidCredentialsError`
135+
turns a wrong password into a crash instead of a form message.
136+
137+
| Operation | Must do this |
138+
| --- | --- |
139+
| `fetchSession` | Return `null` when signed out, never throw |
140+
| `login` | Throw `InvalidCredentialsError` for a bad password, `RateLimitedError` when the limiter refuses |
141+
| `fetchUsers` | Throw `UnauthorizedError` when the session is gone |
142+
| `createUser` | Throw `UnauthorizedError`, `EmailTakenError` for a duplicate email, `ValidationError` for bad input |
143+
| `setUserDisabled` | Throw `UnauthorizedError` when the session is gone |
144+
145+
`InvalidCredentialsError`, `RateLimitedError` and `UnauthorizedError`
146+
come from the main entry point. `EmailTakenError` and
147+
`ValidationError` come from `/admin`.
148+
149+
`UnauthorizedError` deserves particular care, because it is what
150+
[`createAuthQueryClient`](#the-gate) watches for to drop the session
151+
and bring the login screen back. Throw the wrong class from your
152+
transport and a revoked session goes unnoticed.
153+
154+
### One setting for the whole module
155+
156+
The transport is stored in a module-level variable, not in React
157+
context, which has two practical consequences.
158+
159+
Configure it once at startup. There is no provider to place and no
160+
way to give different parts of your tree different transports.
161+
162+
More importantly, the setting belongs to one copy of the package. If
163+
your bundle somehow contains two copies of `@gopherium/react-auth`,
164+
each has its own transport, and one of them silently keeps using
165+
REST. Add the package to your bundler's dedupe list along with
166+
`react` and `@tanstack/react-query`.
167+
168+
`resetAuthTransport()` puts every operation back to REST. It exists
169+
for tests, so one spec's transport cannot leak into the next, and
170+
belongs in a `beforeEach`.
171+
71172
## Testing components that authenticate
72173

73-
The `/testing` entry owns the msw server and its vitest lifecycle, so
74-
app harnesses compose instead of re-creating:
174+
Components behind a login are awkward to test, because every one of
175+
them wants a session and a backend. The `/testing` entry provides
176+
both. It runs a fake HTTP server, using
177+
[msw](https://mswjs.io/), and manages that server's setup and
178+
teardown for you:
75179

76180
```tsx
77181
import {
@@ -83,13 +187,23 @@ import {
83187
} from '@gopherium/react-auth/testing'
84188
```
85189

86-
`installTestEnvironment()` installs the server lifecycle and DOM
87-
cleanup. `seedSession(client, user)` renders components signed in
88-
without a network round trip. The canned handlers cover every auth
89-
endpoint outcome, from `loginOk` to `loginRateLimited`, so specs
90-
declare intent instead of re-typing response literals.
190+
`installTestEnvironment()` starts and stops the server around your
191+
tests and cleans up the DOM between them.
192+
193+
`seedSession(client, user)` puts a signed-in user straight into the
194+
query cache, so a test can render a component behind the gate without
195+
logging in over the network first.
196+
197+
The canned handlers cover every outcome each endpoint can produce,
198+
from `loginOk` to `loginRateLimited`. Naming one says what the test
199+
is about, which reads better than pasting a response body into every
200+
spec.
201+
202+
Two things you have to set up on your side:
91203

92-
Two integration requirements: register your matcher and stub setup
93-
against your own test runner in your harness file, and give your
94-
bundler a dedupe rule for `react` and `@tanstack/react-query` so a
95-
workspace-linked copy never splits the query-client context.
204+
- Register the matchers and stubs your own test runner needs, in your
205+
own setup file.
206+
- Tell your bundler to dedupe `react` and `@tanstack/react-query`.
207+
Without it, a workspace-linked copy of the package can end up with
208+
its own React Query context, and your components will not see the
209+
session you seeded.

0 commit comments

Comments
 (0)