Rest Authentication
All network requests are run through the getRequestInit optionally defined in your RestEndpoint.
Cookie Auth (credentials)
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.