Админка за 5 минут. Фронтэнд — react-admin, бэкэнд — Flask-RESTful

  • Tutorial


Если нужно на коленке получить быстро админку, где фронтендом будет react-admin, а бэкендом Flask-RESTful api, то ниже минимальный код в несколько десятков строк, чтобы это реализовать.

Бэкенд Flask-RESTful api


Сам код состоит из одного файла main.py:

from flask import Flask, request
from flask_restful import Resource,  Api
from flask_jwt_extended import JWTManager
from flask_jwt_extended import create_access_token, jwt_required
from flask_cors import CORS

app = Flask(__name__)

app.config['JWT_SECRET_KEY'] = 'my_cool_secret'
jwt = JWTManager(app)
CORS(app)
api = Api(app)


class UserLogin(Resource):
    def post(self):
        username = request.get_json()['username']
        password = request.get_json()['password']
        if username == 'admin' and password == 'habr':
            access_token = create_access_token(identity={
                'role': 'admin',
            }, expires_delta=False)
            result = {'token': access_token}
            return result
        return {'error': 'Invalid username and password'}


class ProtectArea(Resource):
    @jwt_required
    def get(self):
        return {'answer': 42}


api.add_resource(UserLogin, '/api/login/')
api.add_resource(ProtectArea, '/api/protect-area/')

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')

Пробежимся по коду:

  • Все взаимодействие с внешним миром наш бэкэнд будет осуществлять только посредством RESTful api, даже авторизация в админке тоже через него. Для этого у flask есть удобный модуль: Flask-RESTful api
  • Модуль flask_jwt_extended нам послужит для защиты тех роутов, доступ к которым можно получить только после авторизации. Ничего сакрального тут нет, просто в заголовок (header) к каждому http запросу будет добавляться токен jwt ( JSON Web Token), по которому наше приложение будет понимать, что юзер авторизован.
    В коде выше видно, что используется декоратор @jwt_required для этих целей. Можно его добавлять в те маршруты API, которые должны быть защищены.
  • Без flask_cors мы получим следующую ошибку:
    Access to XMLHttpRequest at 'http://localhost:5000/api/login/' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
    Подробнее о CORS здесь.

Поставим все необходимые библиотеки и запустим код командой:

python main.py

Как видно, я захардкодил логин и пароль к админке: admin / habr.

После того как flask стартанул, можно проверить его работоспособность с помощью curl:

curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "habr"}' localhost:5000/api/login/

Если такой результат:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIU...."
}

Значит все правильно и можно двигаться к фронту.

Фронтэнд react-admin


Мне понравился react-admin. Здесь документация, а тут демо версия:
https://marmelab.com/react-admin-demo/#/login
Логин: demo
Пароль: demo

Чтобы нам получить такую же админку, как в демке, выполняем следующие команды:


git clone https://github.com/marmelab/react-admin.git && cd react-admin && make install   
yarn add axios
make build
make run-demo

Теперь надо ее научить взаимодействовать с нашим бэкендом.

Для этого заменим содержимое файла admin/examples/demo/src/authProvider.js на нижеследующий код, который будет отвечать за авторизацию, за выход из админки и прочее:

admin/examples/demo/src/authProvider.js
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK, AUTH_GET_PERMISSIONS } from 'react-admin';
import axios from 'axios';
import decodeJwt from 'jwt-decode';

export default (type, params) => {

  if (type === AUTH_LOGIN) {
    const { username, password } = params;
    let data = JSON.stringify({ username, password });

    return axios.post('http://localhost:5000/api/login/', data, {
      headers: {
        'Content-Type': 'application/json',
      }
    }).then(res => {
      if (res.data.error || res.status !== 200) {
        throw new Error(res.data.error);
      }
      else {
        const token = res.data.token;
        const decodedToken = decodeJwt(token);
        const role = decodedToken.identity.role;
        localStorage.setItem('token', token);
        localStorage.setItem('role', role);
        return Promise.resolve();
      }
    });
  }

  if (type === AUTH_LOGOUT) {
    localStorage.removeItem('token');
    localStorage.removeItem('role');
    return Promise.resolve();
  }

  if (type === AUTH_ERROR) {
    const { status } = params;
    if (status === 401 || status === 403) {
      localStorage.removeItem('token');
      localStorage.removeItem('role');
      return Promise.reject();
    }
    return Promise.resolve();
  }

  if (type === AUTH_CHECK) {
    return localStorage.getItem('token') ? Promise.resolve() : Promise.reject({ redirectTo: '/login' });
  }

  if (type === AUTH_GET_PERMISSIONS) {
    const role = localStorage.getItem('role');
    return role ? Promise.resolve(role) : Promise.reject();
  }

};


И теперь для прикола обратимся к нашему бэкенду, к роуту: /api/protect-area/ и полученный результат воткнем на главной странице админки, там, где бородатые мужики.

Для этого заменим содержимое файла react-admin/examples/demo/src/dashboard/Welcome.js на вот такой код:

admin/examples/demo/src/dashboard/Welcome.js
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import HomeIcon from '@material-ui/icons/Home';
import CodeIcon from '@material-ui/icons/Code';
import { makeStyles } from '@material-ui/core/styles';
import { useTranslate } from 'react-admin';

const useStyles = makeStyles({
  media: {
    height: '18em',
  },
});

const mediaUrl = `https://marmelab.com/posters/beard-${parseInt(
  Math.random() * 10,
  10
) + 1}.jpeg`;

const Welcome = () => {

  const [state, setState] = useState({});
  const fetchFlask = useCallback(async () => {
    axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('token');
    await axios.get('http://localhost:5000/api/protect-area/').then(res => {
      const answer = res.data.answer;
      setState({ answer });
    });
  }, []);

  useEffect(() => {
    fetchFlask();
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  const translate = useTranslate();
  const classes = useStyles();


  return (
    <Card>
      <CardMedia image={mediaUrl} className={classes.media} />
      <CardContent>
        <Typography variant="h5" component="h2">
          {state.answer}
        </Typography>
        <Typography component="p">
          {translate('pos.dashboard.welcome.subtitle')}
        </Typography>
      </CardContent>
      <CardActions style={{ justifyContent: 'flex-end' }}>
        <Button href="https://marmelab.com/react-admin">
          <HomeIcon style={{ paddingRight: '0.5em' }} />
          {translate('pos.dashboard.welcome.aor_button')}
        </Button>
        <Button href="https://github.com/marmelab/react-admin/tree/master/examples/demo">
          <CodeIcon style={{ paddingRight: '0.5em' }} />
          {translate('pos.dashboard.welcome.demo_button')}
        </Button>
      </CardActions>
    </Card>
  );
};

export default Welcome;


Зайдем на адрес:

localhost:3000
Авторизуемся, введя логин / пасс: admin / habr

И если все норм, то увидим 42 в заголовке на главной странице.

Типа вот так:



Дополнительно


  • Помимо Flask-RESTful есть еще Flask-RESTplus, тут можно глянуть обсуждение, что лучше или хуже
  • Можно написать админку на фронте, дальше запустить: npm run build — получатся готовые статические файлы, которые flask может отдавать просто как темплейт. Подробнее здесь. И таким образом можно избавится от необходимости держать запущенным веб-сервер, отвечающий за react.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 39

    +8
    Админка за 0 минут — запустите любой Database-клиент. Я так некоторые проекты годами веду, очень удобно, все возможности.

    Restfull-админка — это неудобно, потому что элементарные сущности и так можно добавлять в базу, а что-то более сложно все равно придется допиливать.
      0
      Админкой пользуются не только люди, которые умеют с бд клиентом работать. Кроме того в таких приложениях часто присутствует какая-то логика помимо просто записей в базе и не на уровне бд.
        0

        Хранимые процедуры и вот вам логика

          +5
          Во многих проектах стараются их избегать. Потому что уйдет человек, которых их писал и это очень тяжело поддерживать. И с версионированием этого всего как-то непонятно. И удобнее когда вся логика в одном месте — в приложении.
            +4
            Каким образом хранимые процедуры могут, например, отправить письма или сделать https запрос?
              –2
              Легко, в MS SQL есть CLR-сборки, по сути вызов функций из DLL.
              Source: Работаю с биллинговой системой, построенной подобным образом.
                –1
                Ты же просто хранимая процедура, имитация ЯП. Разве может хранимая процедура отправить письмо или сделать http-запрос?
              +1
              Требования этих людей к админкам порой настолько дикие, что приходится выкидывать весь фреймворк для для построения админок и писать ручками. Сомневаюсь что за 10 лет, когда я активно искал хорошее решение для PHP что-то поменялось. Я имею в виду условия когда вы не можете диктовать заказчику как должна выглядеть админка.
                0
                Джанговская админка обычно всех устраивает и там есть всё что угодно из коробки или на pypi. И кастомизируется как угодно.
                  0
                  Окей, классический пример. Есть форма с допдауном. В зависимости от выбора дропдауна подгружается динамически кусок другой формы. Некоторые подформы формы имеют в своей основе сущности из БД, некоторые нет, некоторые имеют сущность в своей основе, но и коллбэки на отрисовку через внешний API. В зависимости от некоторых данных введенных в одной из подформ, автоматически заполняются поля в основной форме.

                  Опишите пожалуйста как вы будете делать такое чучело на любой технологии, правда интересно. Потому что мне еще не попадался достаточный уровень декларативности описания такой вот формы доставки, чтобы я мог не дописывать куски управления формой ручками. И если покажете хотя бы подход или хороший проект на гитхабе, то буду премного благодарен.
                    0
                    Какой-то абстрактный пример. В админке django modelform можно кастомизировать, какие там должны быть поля и как их потом сохранить в бд Остальное накручивается на js.
                      +1
                      Так вот хотелось бы без этой бесконечной накрутки на JS, вернее чтобы фреймворк сам ее делал. Пример не абстрактный — обычная форма доставки с расчетом стоимости разными ТК и заполнением ФИО учетки из адреса доставки.
                        0
                        Ну стоимость можно сделать readonly полем и после сохранения высчитывать её. ФИО тоже после сохранения заполнять.
            0
            Стоит добавить в проект webargs и marshmallow. С ними делать REST под flask прям хорошо и прекрасно.
              0
              Спасибо за коммент. А вроде есть reqparse в flask_restful.
              https://flask-restful.readthedocs.io/en/latest/api.html#module-reqparse
              Нужен ли webargs?

              А по-поводу marshmallow, я к нему присматриваюсь. И раз вы здесь его упомянули хотелось бы спросить: у вас есть позитивный опыт использования? Можете что-нибудь про него рассказать хорошее?
                +1
                webargs позволяет задавать параметры к запросу декоратором, что с моей точки зрения удобнее. Плюс он всеядный, если специально не указывать из какого источника брать ему можно присылать в любом виде, т.е. и query params и form-data и json. Он все обработает одинаково, главное чтобы имена совпадали.
                Дополнительно он имеет отличную интеграцию с marshmallow, что позволяет прям объекты напрямую из запроса доставать.
                Marshmallow я использую и это единственный на данный момент сериализатор под python который нормально из PostgreSQL жрет нативные uuid. Дополнительно там есть слой совместимости с sqlalchemy. Который мне правда не актуален, я использую PonyORM.
                  0
                  Мне больше нравится pydantic. У себя я превращаю аргументы функции в параметры запроса так:
                  from pydantic import create_model
                  def get_query_schema(handler):
                      params = inspect.signature(handler).parameters
                      query_params = {k: (p.annotation, p.default) for k, p in params.items() if k not in ('pk', 'request', 'self')}
                      return create_model('query_schema', **query_params)
                  

                  это можно использовать потом в декораторе или middleware
                  @web.middleware
                  async def webapi_validate_query(request, handler):
                      self = handler.__closure__[0].cell_contents.__self__
                      if request.method not in ('GET', 'POST', 'PUT', 'DELETE'):
                          raise web.HTTPMethodNotAllowed(f'{request.method} not allowed')
                      query = request.query.copy()
                      if self.paginator:
                          self.paginator.get_page_from_query(query)
                      if self.filter_class:
                          self.filter = self.filter_class(**request.query)
                      validated_query = self.query_schema(**query.items()).dict()
                      result = await handler(request, **request.match_info, **validated_query)
                      return web.json_response(result, dumps=dumps)
                    0
                    Вот не лень писать столько кода? Смотрите как это выглядит в случае webargs

                    @bp.route('/charge', methods=['POST'])
                    @use_args({
                        "account": fields.Int(required=True),
                        "agent": fields.Int(required=True),
                        "ts": fields.DateTime(),
                        "unit": fields.Int(required=True),
                        "service": fields.UUID(required=True),
                        "amount": fields.Decimal(required=True),
                        "count": fields.Decimal(missing=1),
                        "note": fields.Str()
                    })
                    def add_charge(args):
                        schema = ChargeMaSchema()
                        if args.get('amount') < 0:
                            return abort(422)
                        try:
                            new_charge = Charge(**args)
                            commit()
                            return schema.jsonify(new_charge)
                        except:
                            abort(422)
                    

                    К примеру кусок кода из моего проекта. Аннотацию входных данных можно так же брать из схемы marshmallow.

                    На abort дополнительно можно довесить обработчик, в webargs если требуется так же можно его довесить.
                      0
                      Так это код из моей библиотеки. А в самом проекте будет просто
                      async def test_handler(request, query: str, page: int=1): pass

                      Мне нравится использовать аннотации типов, этим pydantic больше нравится. В fastapi он аналогично используется
                        0
                        А что делать в случае если надо обрабатывать не только query?
                          0
                          Обычно, в rest, data это данные модели, для которых тоже есть сериализатор. Ну или писать отдельный.
                            0
                            Ну в случае использования webargs это там сразу из коробки, чем оно и хорошо.
                    0
                    norguhtar, спасибо за наводку.
                    Получается, что если мы хотим использовать webargs в примере из статьи.
                    То писать надо примерно так:
                    from webargs.flaskparser import use_args                                                                                                                                 
                    from webargs import fields    
                    # Code ...
                    class UserLogin(Resource):                                                                                                                                               
                          @use_args({'username': fields.Str(),                                                                                                                                 
                                     'password': fields.Str()}, locations=['json'])                                                                                                            
                          def post(self, args):                                                                                                                                                
                              if args.get('username') == 'admin' and args.get('password') == 'habr': 
                                 # Code
                    
                    

                    Верно? Я проверил, в принципе, работает.
                      0
                      Да. Только еще стоит добавить:

                        @use_args({
                      'username': fields.Str(required=True),                                                                                                                                 
                      'password': fields.Str(required=True)}, 
                      locations=['json'])    
                      


                      В этом случае если параметры не обнаружены, вывалит ошибку по умолчанию. Туда можно вставить свой хендлер обработки и обрабатывать отсутствие параметров единожды. Там еще есть опция missing которая позволяет задавать умолчания, с ней надо учесть, что умолчания там задаются один раз при старте. А то я там положил раз datetime.now() :)
                        0
                        Допустим удалим один или несколько параметров, то мы получаем в json
                        {
                            "error": "Invalid username and password"
                        }


                        А если есть required=True, то получим
                        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
                        <title>422 Unprocessable Entity</title>
                        <h1>Unprocessable Entity</h1>
                        <p>The request was well-formed but was unable to be followed due to semantic errors.</p>
                        


                        Видимо, надо по ситуации все же решать. Либо как-то научить его отдавать json в качестве исключения.
                          0
                          Это отрабатывает глобальный хендлер. Его можно переопределить и это более правильный вариант обработки ошибок. Для flask webargs.readthedocs.io/en/latest/framework_support.html#flask
                          Если хочется отдавать 200 то так
                          webargs.readthedocs.io/en/latest/advanced.html#returning-http-400-responses

                          И все. Обработка ошибок делается один раз и выводит то что надо.
                            0
                            Не, я к тому, что react выведет ошибку «Invalid username and password», а в случае с required=True выведет «Network error».
                            Вот что будет в браузере.
                              0
                              А я вам и говорю, что поведение это изменяемое. И да 422 ошибка это не Network error. Она должна обрабатываться на стороне react особенно учитывая что подразумевается REST а там кодами отличными от 200 вообще-то пользоваться надо.
                  +1
                  Просто годная библиотека для сериализации. Кстати, я тут сделал генераторы маршмаллов из моделей peewee и алхимии, может кому пригодится
                  github.com/pawnhearts/aiorf/blob/master/aiorf/modelschema.py
                  github.com/pawnhearts/aiorf/blob/master/aiorf/saschema.py
                0
                Потратил 3 часа на то, чтобы разобраться почему шаг в сторорону от стандартного туториала делает так что не работает ничего. Либо GuesserList выдает ошибку, либо самописный List вообще не делает запросов. Как отлаживать этого монстра вообще ума не приложу.
                  0
                  Можно попробовать начать с simple:
                  codesandbox.io/s/github/marmelab/react-admin/tree/master/examples/simple

                    0
                    Можно было бы хотя бы попробовать сделать дебаггинг сетевых запросов, почему например тот же файл с JSON из туториала сохраненный на локальном сервере, сразу выдает ошибку что пользователи не найдены. При этом дебаг запросов есть только в FakeDataProvider, где он и так не нужен.
                      0
                      Ну, да. Я на это тоже посмотрел, разбираться поленился и просто добавил axios.
                        0
                        В итоге помогло добавление в ответ заголовка:

                        "Access-Control-Allow-Origin: адрес.сайта.на.которомюадминка.com"

                        Такое надо писать большими красными буквами, а ни в одном дефолтном провайдере это не написано.
                0
                Довелось мне поработать с react-admin.
                Это какой-то сборник антипаттернов, а не фреймворк для админок.
                Сойдет, если заказчика устраивает функционал точь в точь, как в демке. Шаг в сторону, получаем проблему на проблеме.
                  0
                  Спасибо за инфу.
                  Можете порекомендовать альтернативу?
                    0
                    Сомневаюсь, что есть хорошие альтернативы.

                    Я не пробовал, но встречал следующие:
                    cxjs.io — выглядит довольно интересно;
                    pro.ant.design

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое