Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle error during api documentation parsing #51

Merged

Conversation

mauchede
Copy link
Contributor

@mauchede mauchede commented Sep 6, 2017

This PR should fix issues #43 and #31.


This PR handles parsing errors (see my PR on dunglas/api-doc-parser):

  • if the parsing is resolved, the "classic" behavior is kept (rendering AdminBuilder).
  • if the parsing is rejected, a message will be shown.

In all cases, the user has the possibility to define some customRoutes. These routes will be added to the customRoutes defined via properties.


With this PR, we can now use api-platform/admin on a fully secured API. For example:

import { AUTH_ERROR, AUTH_LOGIN, AUTH_LOGOUT } from 'admin-on-rest';
import parseHydraDocumentation from 'api-doc-parser/lib/hydra/parseHydraDocumentation';
import { fetchHydra as baseFetchHydra, HydraAdmin, hydraClient as baseHydraClient } from 'api-platform-admin';
import React from 'react';
import { Redirect } from 'react-router-dom';

// Define constants

const ENTRYPOINT = 'http://127.0.0.1:8000';
const LOCAL_STORAGE_KEY_TOKEN = 'token';

// Define restClient

const fetchHeaders = {
    'Authorization': `Bearer ${window.localStorage.getItem(LOCAL_STORAGE_KEY_TOKEN)}`,
};

const fetchHydra = (url, options = {}) => baseFetchHydra(url, {
    ...options,
    headers: new Headers(fetchHeaders),
});

const restClient = api => baseHydraClient(api, fetchHydra);

// Define authClient

const authClient = (type, params) => {
    switch (type) {
        case AUTH_ERROR:
            if (401 === params.status || 403 === params.status) {
                window.localStorage.removeItem(LOCAL_STORAGE_KEY_TOKEN);
                window.location.reload();
                break;
            }

            return Promise.resolve();

        case AUTH_LOGIN:
            const body = new FormData();
            body.append('username', params.username);
            body.append('password', params.password);

            const request = new Request(`${ENTRYPOINT}/login`, {
                method: 'POST',
                body,
            });

            return fetch(request)
                .then(response => {
                    if (response.status < 200 || response.status >= 300) {
                        throw new Error(response.statusText);
                    }

                    return response.json();
                })
                .then(({ token }) => {
                    window.localStorage.setItem(LOCAL_STORAGE_KEY_TOKEN, token);
                    window.location.replace('/');
                });

        case AUTH_LOGOUT:
            window.localStorage.removeItem(LOCAL_STORAGE_KEY_TOKEN);

            return Promise.resolve();

        default:
            return Promise.resolve();
    }
};

// Define apiDocumentationParser

const apiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint, { headers: new Headers(fetchHeaders) })
    .then(
        ({ api }) => ({ api }),
        (result) => {
            switch (result.status) {
                case 401:
                    return Promise.resolve({
                        api: result.api,
                        customRoutes: [
                            {
                                props: {
                                    path: '/',
                                    render: () => (
                                        <Redirect to={`/login`}/>
                                    ),
                                },
                            },
                        ],
                    });

                default:
                    return Promise.reject(result);
            }
        },
    );


// Render administration

export default props => (
    <HydraAdmin
        apiDocumentationParser={apiDocumentationParser}
        authClient={authClient}
        entrypoint={ENTRYPOINT}
        restClient={restClient}
    />
);

In this example:

  • we add a customRoute if parsing returned a 401 error. This route will redirect to the login page.
  • the authClient will refresh current page when a JWT token will be stored. Refresh the page will give the possibility to parse again the documentation, but this time the JWT token will be sent.

@mauchede mauchede force-pushed the task/handle-api-doc-parser-error branch from 387cb3b to 985f62d Compare September 7, 2017 08:23
Copy link
Contributor

@Simperfit Simperfit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @mauchede that will be useful !

.then(api => this.setState({api}));
this.props.apiDocumentationParser(this.props.entrypoint).then(
({api, customRoutes = []}) =>
this.setState({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels duplicated, is it possible to not duplicate it or it's not worth the complexity ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of call setState directly, we could "normalize" the data:

    this.props
      .apiDocumentationParser(this.props.entrypoint)
      .then(
        ({api, customRoutes = []}) => ({
          api,
          customRoutes,
          hasError: false,
          loaded: true,
        }),
        ({api, customRoutes = []}) => ({
          api,
          customRoutes,
          hasError: true,
          loaded: true,
        }),
      )
      .then(state => this.setState(state));

return <span>Loading...</span>;
}

if (true === this.state.hasError) {
return <span>Impossible to retrieve API documentation.</span>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this could be a key instead of directly english ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing to pass props as suggested by @Gregcop1 looks better to me.

}

render() {
if (null === this.state.api) {
if (false === this.state.loaded) {
return <span>Loading...</span>;
Copy link

@Gregcop1 Gregcop1 Sep 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to make this more configurable and user friendly, it could be possible to pass messages (loading, error, ...) on props with default values. So my french interface can keep french messages

}

render() {
if (null === this.state.api) {
if (false === this.state.loaded) {
return <span>Loading...</span>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should add a className here to allow customization of the render

return <span>Loading...</span>;
}

if (true === this.state.hasError) {
return <span>Impossible to retrieve API documentation.</span>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should add a className here to allow customization of the render

this.props
.apiDocumentationParser(this.props.entrypoint)
.then(api => this.setState({api}));
this.props.apiDocumentationParser(this.props.entrypoint).then(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why .then on the same line?

Copy link
Member

@dunglas dunglas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling @Gregcop1 suggestions would be nice but 👍 , great job!

@mauchede mauchede force-pushed the task/handle-api-doc-parser-error branch from 985f62d to 30c50b1 Compare September 7, 2017 12:41
loaded: true,
}),
)
.then(state => this.setState(state));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried .then(this.setState) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried and the following error occurred:

Uncaught (in promise) TypeError: Cannot read property 'updater' of undefined

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works, if I use:

.then(this.setState.bind(this))

@mauchede mauchede force-pushed the task/handle-api-doc-parser-error branch from 30c50b1 to 001596f Compare September 7, 2017 13:03
@dunglas dunglas merged commit 41c97aa into api-platform:master Sep 7, 2017
@dunglas
Copy link
Member

dunglas commented Sep 7, 2017

Thanks @mauchede!

@Superprogrameur59
Copy link

It could be very useful to put this example in the official doc of api platform because this "https://api-platform.com/docs/admin/authentication-support" doc is wrong ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants