Rest Authentication
All network requests are run through the getRequestInit optionally defined in your RestEndpoint.
Cookie Auth
Here's an example using simple cookie auth:
import { RestEndpoint } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
getRequestInit(body: any): RequestInit {
return {
...super.getRequestInit(body),
credentials: 'same-origin',
};
}
}
import { createResource, Entity } from '@data-client/rest';
import AuthdEndpoint from 'api/AuthdEndpoint';
class MyEntity extends Entity {
/* Define MyEntity */
}
export const MyResource = createResource({
path: '/my/:id',
schema: MyEntity,
Endpoint: AuthdEndpoint,
});
Access Tokens or JWT
- static member
- function singleton
- async function
import { RestEndpoint } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
declare static accessToken?: string;
getHeaders(headers: HeadersInit) {
return {
...headers,
'Access-Token': this.constructor.accessToken,
};
}
}
Upon login we set the token:
import AuthdEndpoint from 'api/AuthdEndpoint';
function Auth() {
const handleLogin = useCallback(
async e => {
const { accessToken } = await login(new FormData(e.target));
// success!
AuthdEndpoint.accessToken = accessToken;
},
[login],
);
return <AuthForm onSubmit={handleLogin} />;
}
import { getAuthToken } from 'authorization-singleton';
import { RestEndpoint } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async getHeaders(headers: HeadersInit) {
return {
...headers,
'Access-Token': await getAuthToken(),
};
}
}
Upon login we set the token:
import { setAuthToken } from 'authorization-singleton';
import AuthdResource from 'resources/AuthdResource';
function Auth() {
const handleLogin = useCallback(
async e => {
const { accessToken } = await login(new FormData(e.target));
// success!
setAuthToken(accessToken);
},
[login],
);
return <AuthForm onSubmit={handleLogin} />;
}
import { getAuthToken } from 'authorization-singleton';
import { RestEndpoint } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
'Access-Token': getAuthToken(),
};
}
}
Upon login we set the token:
import { setAuthToken } from 'authorization-singleton';
import AuthdResource from 'resources/AuthdResource';
function Auth() {
const handleLogin = useCallback(
async e => {
const { accessToken } = await login(new FormData(e.target));
// success!
setAuthToken(accessToken);
},
[login],
);
return <AuthForm onSubmit={handleLogin} />;
}
import { createResource, Entity } from '@data-client/rest';
import AuthdEndpoint from 'api/AuthdEndpoint';
class MyEntity extends Entity {
/* Define MyEntity */
}
export const MyResource = createResource({
path: '/my/:id',
schema: MyEntity,
Endpoint: AuthdEndpoint,
});
Auth Headers from React Context
Using React Context for state that is not displayed (like auth tokens) is not recommended. This will result in unnecessary re-renders and application complexity.
- Resource
- RestEndpoint
We can transform any Resource into one that uses hooks to create endpoints by using hookifyResource
import { createResource, hookifyResource } from '@data-client/rest';
// Post defined here
export const PostResource = hookifyResource(
createResource({ path: '/posts/:id', schema: Post }),
function useInit(): RequestInit {
const accessToken = useAuthContext();
return {
headers: {
'Access-Token': accessToken,
},
};
},
);
Then we can get the endpoints as hooks in our React Components
import { useSuspense } from '@data-client/react';
import { PostResource } from 'api/Post';
function PostDetail({ id }) {
const post = useSuspense(PostResource.useGet(), { id });
return <div>{post.title}</div>;
}
Using this means all endpoint calls must only occur during a function render.
function CreatePost() {
const controller = useController();
const createPost = PostResource.useCreate();
return (
<form onSubmit={e => controller.fetch(createPost, new FormData(e.target))}>
{/* ... */}
</form>
);
}
We will first provide an easy way of using the context to alter the fetch headers.
import { RestEndpoint } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
declare accessToken?: string;
getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
'Access-Token': this.accessToken,
};
}
}
Next we will extend to generate a new endpoint with this context injected.
function useEndpoint(endpoint: RestEndpoint) {
const accessToken = useAuthContext();
return useMemo(
() => endpoint.extend({ accessToken }),
[endpoint, accessToken],
);
}
Using this means all endpoint calls must only occur during a function render.
function CreatePost() {
const controller = useController();
const createPost = useEndpoint(PostResource.create);
return (
<form
onSubmit={e => controller.fetch(createPost, {}, new FormData(e.target))}
>
{/* ... */}
</form>
);
}
Code organization
If much of your Resources
share a similar auth mechanism, you might
try extending from a base class that defines such common customizations.
401 Logout Handling
In case a users authorization expires, the server will typically responsd to indicate as such. The standard way of doing this is with a 401. LogoutManager can be used to easily trigger any de-authorization cleanup.