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

Если нужно на коленке получить быстро админку, где фронтендом будет 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.


ссылка на оригинал статьи https://habr.com/ru/post/477126/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *