- PVSM.RU - https://www.pvsm.ru -

Если нужно на коленке получить быстро админку, где фронтендом будет react-admin [1], а бэкендом Flask-RESTful api [2], то ниже минимальный код в несколько десятков строк, чтобы это реализовать.
Сам код состоит из одного файла 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')
Пробежимся по коду:
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 здесь [6].
Поставим все необходимые библиотеки и запустим код командой:
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. Здесь документация [7], а тут демо версия:
https://marmelab.com/react-admin-demo/#/login [8]
Логин: 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 на нижеследующий код, который будет отвечать за авторизацию, за выход из админки и прочее:
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 на вот такой код:
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 в заголовке на главной странице.
Типа вот так:

npm run build — получатся готовые статические файлы, которые flask может отдавать просто как темплейт. Подробнее здесь [10]. И таким образом можно избавится от необходимости держать запущенным веб-сервер, отвечающий за react.Автор: pcdesign
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/flask/337730
Ссылки в тексте:
[1] react-admin: https://marmelab.com/react-admin/
[2] Flask-RESTful api: https://flask-restful.readthedocs.io/en/latest/
[3] flask_jwt_extended: https://flask-jwt-extended.readthedocs.io/en/stable/
[4] JSON Web Token: https://ru.wikipedia.org/wiki/JSON_Web_Token
[5] flask_cors: https://flask-cors.readthedocs.io/en/latest/
[6] CORS здесь: https://developer.mozilla.org/ru/docs/Web/HTTP/CORS
[7] документация: https://marmelab.com/react-admin/Readme.html
[8] https://marmelab.com/react-admin-demo/#/login: https://marmelab.com/react-admin-demo/#/login
[9] тут: https://stackoverflow.com/questions/40165665/flask-restful-vs-flask-restplus
[10] здесь: https://stackoverflow.com/questions/44209978/serving-a-create-react-app-with-flask
[11] Источник: https://habr.com/ru/post/477126/?utm_source=habrahabr&utm_medium=rss&utm_campaign=477126
Нажмите здесь для печати.