58 | );
59 | };
60 |
61 | Layout.propTypes = {
62 | children: PropTypes.node,
63 | menu: PropTypes.element,
64 | title: PropTypes.string.isRequired,
65 | theme: PropTypes.object.isRequired,
66 | }
67 |
68 | Layout.defaultProps = {
69 | theme: defaultTheme,
70 | }
71 |
72 | export default Layout;
--------------------------------------------------------------------------------
/website/src/Routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | IndexRoute,
4 | Route,
5 | } from 'react-router';
6 | import HomeIcon from 'material-ui/svg-icons/action/home';
7 | import VisibilityOffIcon from 'material-ui/svg-icons/action/visibility-off';
8 |
9 | import Wrapper from './ui/containers/Wrapper/Wrapper';
10 | import Login from './ui/containers/Login/Login';
11 | import Layout from './ui/containers/Layout/Layout';
12 | import Menu from './ui/containers/Layout/Menu';
13 | import Home from './ui/containers/Home/Home';
14 | import NotFound from './ui/containers/NotFound/NotFound';
15 | import {
16 | AuthRequired,
17 | UnauthRequired,
18 | } from './ui/containers/Auth';
19 | import {
20 | HOME_ROUTE,
21 | AUTH,
22 | LOGIN,
23 | } from './common';
24 |
25 | const menuItems = [
26 | {
27 | name: "Home",
28 | path: HOME_ROUTE,
29 | icon: HomeIcon,
30 | },
31 | {
32 | name: "Nowhere",
33 | path: '/no-where',
34 | icon: VisibilityOffIcon,
35 | },
36 | ];
37 |
38 | const MenuComponent = () => (
39 |
40 | );
41 |
42 | const LayoutComponent = ({children}) => {
43 | return (
44 | }
47 | >
48 | { children }
49 |
50 | );
51 | };
52 |
53 | const getRoutes = () => {
54 | return (
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default getRoutes;
--------------------------------------------------------------------------------
/flask-api/flask_api/user/manager.py:
--------------------------------------------------------------------------------
1 | from flask_script import Manager, prompt, prompt_pass, prompt_choices
2 | from flask_security.utils import encrypt_password
3 | from .helpers import user_datastore
4 |
5 |
6 | manager = Manager(usage="Perform user database management")
7 |
8 |
9 | @manager.command
10 | def create_role():
11 | """
12 | Creates a role in the database"
13 | """
14 | name = prompt("Please enter the name of the Role ?", default='user')
15 | user_datastore.find_or_create_role(name)
16 | user_datastore.commit()
17 |
18 |
19 | @manager.command
20 | def create_user():
21 | """
22 | Creates a user in the database
23 | """
24 | email = prompt("Please enter your email address ?", default='user.name@domain.com')
25 |
26 | password_match = False
27 | while not password_match:
28 | password = prompt_pass("Please enter your password ?", default='password')
29 | confirm_password = prompt_pass("Please confirm your password ?", default='password')
30 | password_match = password == confirm_password
31 |
32 | role = prompt_choices("Please enter your role ?",
33 | choices=[role.name for role in user_datastore.role_model.query],
34 | resolve=str,
35 | default='user')
36 |
37 | first_name = prompt("Please enter your first name ?", default="user")
38 | last_name = prompt("Please enter your first name ?", default="name")
39 | user_name = prompt("Please enter your first name ?", default="uname")
40 |
41 | user_datastore.create_user(email=email,
42 | password=encrypt_password(password),
43 | roles=[role],
44 | first_name=first_name.capitalize(),
45 | last_name=last_name.capitalize(),
46 | user_name=user_name)
47 | user_datastore.commit()
48 |
--------------------------------------------------------------------------------
/flask-api/config/cfg.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import datetime
3 | import os
4 | from os.path import dirname, abspath
5 |
6 |
7 | class BaseConfig:
8 | APP_NAME = 'Flask-React Boilerplate'
9 | SERVER_NAME = 'localhost:5000'
10 |
11 | # Define the application directory
12 | BASE_DIR = abspath(dirname(dirname(__file__)))
13 |
14 |
15 | class DatabaseConfig:
16 | # Define the database
17 | SQLALCHEMY_TRACK_MODIFICATIONS = True
18 | DATABASE_CONNECT_OPTIONS = {}
19 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BaseConfig.BASE_DIR, 'app.db')
20 |
21 |
22 | class SessionCookieConfig:
23 | PERMANENT_SESSION_LIFETIME = datetime.timedelta(days=7)
24 | SESSION_COOKIE_HTTPONLY = False
25 |
26 |
27 | class CSRFConfig:
28 | # Enable protection agains *Cross-site Request Forgery (CSRF)*
29 | CSRF_ENABLED = True
30 | WTF_CSRF_FIELD_NAME = 'csrfToken'
31 | WTF_CSRF_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE']
32 |
33 |
34 | class PasswordSecurityConfig:
35 | # Config related to password security
36 | SECURITY_PASSWORD_HASH = 'bcrypt'
37 | SECURITY_PASSWORD_MINIMAL_LENGTH = 8
38 |
39 |
40 | class DevelopmentConfig(BaseConfig, DatabaseConfig, SessionCookieConfig, CSRFConfig, PasswordSecurityConfig):
41 | DEBUG = True
42 |
43 |
44 | config = {
45 | 'development': DevelopmentConfig,
46 | }
47 |
48 |
49 | def set_config(app, config_name='development'):
50 | if not os.path.isfile('config/secrets.cfg'):
51 | from config.secret_generator import generate_secrets_file
52 | generate_secrets_file('config/secrets.cfg')
53 |
54 | secrets_config = read_config_file('config/secrets.cfg')
55 |
56 | app.config.from_object(config[config_name])
57 | app.config = {**app.config, **secrets_config}
58 |
59 | return app.config
60 |
61 |
62 | def read_config_file(path):
63 | if not os.path.isfile(path):
64 | raise FileExistsError(path + ' configuration file does not exist. Please create it.')
65 |
66 | config_parser = configparser.ConfigParser()
67 | config_parser.read(path)
68 | return {key.upper(): config_parser[section][key] for section in config_parser.sections() for key in config_parser[section]}
69 |
--------------------------------------------------------------------------------
/website/src/redux/actions/auth.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Authentification actions
3 | */
4 | import {
5 | GET,
6 | POST,
7 | PUT,
8 | } from '../../APIManager';
9 | import {
10 | AUTH_API_BASE_URL,
11 | LOAD_AUTH_RESOURCE,
12 | LOGIN_RESOURCE,
13 | LOGOUT_RESOURCE,
14 | } from '../../common';
15 |
16 | /* For loading user authentification from server */
17 | export const LOAD_AUTH = 'LOAD_AUTH';
18 | export const LOAD_AUTH_REQUEST = 'LOAD_AUTH_REQUEST';
19 | export const LOAD_AUTH_SUCCESS = 'LOAD_AUTH_SUCCESS';
20 | export const LOAD_AUTH_FAILURE = 'LOAD_AUTH_FAILURE';
21 | export const LOAD_AUTH_CANCEL = 'LOAD_AUTH_CANCEL';
22 |
23 | /**
24 | * Load authentification action creator
25 | */
26 | export const loadAuth = () => ({
27 | type: LOAD_AUTH,
28 | meta: {
29 | APIBaseUrl: AUTH_API_BASE_URL,
30 | resource: LOAD_AUTH_RESOURCE,
31 | requestType: GET,
32 | }
33 | })
34 |
35 | /* For login user on server */
36 | export const LOGIN = 'LOGIN';
37 | export const LOGIN_REQUEST = 'LOGIN_REQUEST';
38 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
39 | export const LOGIN_FAILURE = 'LOGIN_FAILURE';
40 | export const LOGIN_CANCEL = 'LOGIN_CANCEL';
41 |
42 | /**
43 | * Login action creator
44 | * @param {object} values - Login credentials
45 | * @param {string} [formName] - if the login action is dispatched using redux-form ()
46 | * @param {string} [csrfToken] - csrf token
47 | */
48 | export const login = (values, formName, csrfToken) => ({
49 | type: LOGIN,
50 | payload: values,
51 | meta: {
52 | APIBaseUrl: AUTH_API_BASE_URL,
53 | resource: LOGIN_RESOURCE,
54 | requestType: POST,
55 | formName,
56 | csrfToken,
57 | },
58 | })
59 |
60 | /* For logout user on server */
61 | export const LOGOUT= 'LOGOUT';
62 | export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
63 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
64 | export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
65 | export const LOGOUT_CANCEL = 'LOGOUT_CANCEL';
66 |
67 | /**
68 | * Logout action creator
69 | * @param {string} [csrfToken] csrf token
70 | */
71 | export const logout = (csrfToken) => ({
72 | type: LOGOUT,
73 | meta: {
74 | APIBaseUrl: AUTH_API_BASE_URL,
75 | resource: LOGOUT_RESOURCE,
76 | requestType: PUT,
77 | csrfToken,
78 | },
79 | })
80 |
81 | export const AUTH_ACTION_TYPES = [LOAD_AUTH, LOGIN, LOGOUT]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flask-React-Boilerplate
2 | This project has been developped in order to gather best practices at
3 | - **Building React Single Page Applications** that allow to fetch data from Web APIs
4 | - **Building Python Flask REST APIs** that efficiently and safely expose data to the Web
5 |
6 | This project is intended for developpers who desire to accomodate with React and/or Flask technologies. It covers a large panel of librairies and aims to include best practices at developping robust Web Applications.
7 |
8 | This boilerplate is made of 2 independant projects one for React Single Page Application and one for Python Flask API. A reader interested in only one of these 2 technologies can totally give focus on it without meeting any misunderstanding. Nevertheless both projects are designed to be compatible.
9 |
10 | ## Packages
11 | ### React
12 | - [Create-React-App](https://github.com/facebookincubator/create-react-app) - Facebook project intended to easily package React Applications
13 | - [Redux](https://github.com/reactjs/redux) - Very popular package that allows proper Application State management
14 | - [React-Router](https://github.com/ReactTraining/react-router) - Package that allows to dynamically manage Applications Route
15 | - [Redux-Saga](https://github.com/redux-saga/redux-saga) - Package that properly handles side effects (e.g. asynchronous fetch calls)
16 | - [Redux-Form](https://github.com/erikras/redux-form) - Package that allows to easily synchonize forms and Redux state
17 | - [Material-UI](https://github.com/callemall/material-ui) - Library of React components that implements *Google Material Design* specification
18 |
19 | ### Python
20 | - [Flask](https://github.com/pallets/flask) - Python microframework for Web development.
21 | - [Flask-RESTful](https://github.com/flask-restful/flask-restful) - Flask extension that allows to easily expose REST APIs
22 | - [Flask-Login](https://github.com/maxcountryman/flask-login) - Flask extension that manages user session (login, logout, etc.)
23 | - [Flask-WTF](https://github.com/lepture/flask-wtf) - Flask extension that allows to handle forms. It also includes CSRF protection
24 | - [SQLAlchemy](https://github.com/zzzeek/sqlalchemy) - Object Relationship Mapper (ORM) that allows easy dialog with SQL databases
25 | - [Marshmallow](https://github.com/marshmallow-code/marshmallow) - Convenient package to serialize/deserialize Python objects into json format
26 | - [Flask-Script](https://github.com/smurfix/flask-script) - Convenient Flask extension that allows to implement CLI commands
--------------------------------------------------------------------------------
/flask-api/flask_api/user/models.py:
--------------------------------------------------------------------------------
1 | from flask_security import RoleMixin, UserMixin
2 |
3 | from ..extensions import db
4 | from ..common.constants import STRING_LENGTH
5 | from ..user.constants import ROLE_TABLE_NAME, ROLE_NAME_LENGTH, \
6 | USER_TABLE_NAME, USER_EMAIL_LENGTH, USER_PASSWORD_LENGTH, \
7 | USER_LAST_NAME_LENGTH, USER_FIRST_NAME_LENGTH, USER_USER_NAME_LENGTH, \
8 | SEX_TYPES, SEX_OTHER, USER_STATUS, STATUS_NEW
9 |
10 | # n-n mapping table between users and roles
11 | user_role = db.Table('%s_%s' % (USER_TABLE_NAME, ROLE_TABLE_NAME),
12 | db.Column('user_id', db.Integer(), db.ForeignKey('%s.id' % USER_TABLE_NAME)),
13 | db.Column('role_id', db.Integer(), db.ForeignKey('%s.id' % ROLE_TABLE_NAME)))
14 |
15 |
16 | class Role(db.Model, RoleMixin):
17 | __tablename__ = ROLE_TABLE_NAME
18 |
19 | id = db.Column(db.Integer(), primary_key=True)
20 | name = db.Column(db.String(ROLE_NAME_LENGTH), unique=True)
21 | description = db.Column(db.String(STRING_LENGTH))
22 |
23 | def __repr__(self):
24 | return '' % self.name
25 |
26 |
27 | class User(db.Model, UserMixin):
28 | __tablename__ = USER_TABLE_NAME
29 |
30 | id = db.Column(db.Integer, primary_key=True)
31 |
32 | email = db.Column(db.String(USER_EMAIL_LENGTH), nullable=False, unique=True)
33 | password = db.Column(db.String(USER_PASSWORD_LENGTH), nullable=False)
34 |
35 | last_name = db.Column(db.String(USER_LAST_NAME_LENGTH))
36 | first_name = db.Column(db.String(USER_FIRST_NAME_LENGTH))
37 | user_name = db.Column(db.String(USER_USER_NAME_LENGTH))
38 |
39 | _sex = db.Column(db.Integer(), nullable=False, default=SEX_OTHER)
40 |
41 | def _get_sex(self):
42 | return SEX_TYPES.get(self._sex)
43 |
44 | def _set_sex(self, sex):
45 | self._sex = sex
46 |
47 | sex = db.synonym('_sex', descriptor=property(_get_sex, _set_sex))
48 |
49 | active = db.Column(db.Boolean())
50 | _status = db.Column(db.Integer(), nullable=False, default=STATUS_NEW)
51 |
52 | def _get_status(self):
53 | return USER_STATUS.get(self._status)
54 |
55 | def _set_status(self, status):
56 | self._status = status
57 |
58 | status = db.synonym('_status', descriptor=property(_get_status, _set_status))
59 |
60 | confirmed_at = db.Column(db.DateTime())
61 |
62 | roles = db.relationship('Role',
63 | secondary=user_role,
64 | backref=db.backref('users', lazy='dynamic'))
65 |
66 | def __repr__(self):
67 | return '' % self.email
68 |
69 |
--------------------------------------------------------------------------------
/website/src/ui/containers/Layout/UserBox.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | PropTypes,
3 | } from 'react';
4 | import {
5 | connect,
6 | } from 'react-redux';
7 |
8 | import MenuItem from 'material-ui/MenuItem';
9 | import IconMenu from 'material-ui/IconMenu';
10 | import IconButton from 'material-ui/IconButton';
11 | import InputIcon from 'material-ui/svg-icons/action/input';
12 | import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert';
13 | import CircularProgress from 'material-ui/CircularProgress';
14 | import {
15 | getStyles as getAppBarStyles,
16 | } from 'material-ui/AppBar/AppBar';
17 | import {
18 | logout as logoutAction,
19 | setUserBoxVisibility,
20 | } from '../../../redux/actions';
21 |
22 |
23 | const UserBoxIcon = (props, context) => {
24 | const {
25 | onLogout,
26 | open,
27 | auth: {
28 | status: {
29 | logout,
30 | }
31 | },
32 | onRequestChange,
33 | } = props;
34 |
35 | const styles = getAppBarStyles(props, context);
36 |
37 | return (
38 |
41 |
42 |
43 | }
44 | anchorOrigin={{vertical:'bottom', horizontal:'right',}}
45 | targetOrigin={{vertical:'top', horizontal:'right',}}
46 | open={open}
47 | onRequestChange={onRequestChange}
48 | >
49 | : }
53 | />
54 |
55 | );
56 | };
57 |
58 | UserBoxIcon.propTypes = {
59 | onLogout: PropTypes.func.isRequired,
60 | open: PropTypes.bool.isRequired,
61 | logout: PropTypes.oneOfType([
62 | PropTypes.object,
63 | ]),
64 | onRequestChange: PropTypes.func.isRequired
65 | };
66 |
67 | UserBoxIcon.contextTypes = {
68 | muiTheme: PropTypes.object.isRequired,
69 | };
70 |
71 | const mapStateToProps = (state) => ({
72 | auth: state.auth,
73 | open: state.ui.userBoxOpen,
74 | });
75 |
76 | const mapDispatchToProps = (dispatch) => ({
77 | onLogout: (csrfToken) => dispatch(logoutAction(csrfToken)),
78 | onRequestChange: (value) => dispatch(setUserBoxVisibility(value)),
79 | });
80 |
81 | const mergeProps = (stateProps, dispatchProps) => ({
82 | ...stateProps,
83 | ...dispatchProps,
84 | onLogout: () => dispatchProps.onLogout(stateProps.auth.csrfToken),
85 | });
86 |
87 | export default connect(
88 | mapStateToProps,
89 | mapDispatchToProps,
90 | mergeProps
91 | )(UserBoxIcon);
92 |
--------------------------------------------------------------------------------
/website/src/ui/containers/Home/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/website/src/redux/saga/appSaga/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | put,
3 | take,
4 | call,
5 | fork,
6 | takeEvery,
7 | takeLatest,
8 | race,
9 | } from 'redux-saga/effects';
10 | import {
11 | push,
12 | } from 'react-router-redux';
13 |
14 | import createFetchSaga from './fetchSaga';
15 |
16 | import {
17 | loadAuth,
18 | LOAD_AUTH,
19 | LOGIN,
20 | LOGOUT,
21 | FETCH_FAILURE,
22 | AUTH_ACTION_TYPES,
23 | } from '../../actions';
24 | import {
25 | HOME_ROUTE,
26 | AUTH_LOGIN_ROUTE,
27 | } from '../../../common';
28 |
29 | /**
30 | * App Saga creator
31 | * @param {function} APIManager - function that perform API fetch calls
32 | */
33 | function createAppSaga(APIManager) {
34 |
35 | const handleFetch = createFetchSaga(APIManager);
36 | /**
37 | * App saga ran each time a user connects to the app
38 | */
39 | function* appSaga() {
40 | while (true) {
41 | /* Make an API call to load user authentification */
42 | yield fork(handleFetch, loadAuth());
43 |
44 | /* Waits for outcome from the authentification loading task */
45 | const authTaskOutcomeAction = yield take([`${LOAD_AUTH}_FAILURE`, `${LOAD_AUTH}_SUCCESS`]);
46 |
47 | if (authTaskOutcomeAction.type === `${LOAD_AUTH}_FAILURE`) {
48 | /* If Authentification loading failed then user is redirected to the login page */
49 | yield put(push(AUTH_LOGIN_ROUTE));
50 |
51 | function* loginCycle() {
52 | yield takeLatest(LOGIN, handleFetch);
53 | }
54 |
55 | /**
56 | * Starts a race that accepts LOGIN requests and finishes when a succesful login action is dispatched to the server
57 | */
58 | yield race({
59 | loginCycle: call(loginCycle),
60 | loginSuccess: take(`${LOGIN}_SUCCESS`),
61 | });
62 | }
63 |
64 | yield put(push(HOME_ROUTE));
65 |
66 | /**
67 | * Reaching this point means user is correctly logged in.
68 | * From now on user can
69 | * - load authentification
70 | * - perform fetch request
71 | * - logout
72 | * In case we detect
73 | * - a successful logout
74 | * - any fetch authentification error with status 401
75 | */
76 |
77 | function* loadAuthCycle() {
78 | yield takeLatest(LOAD_AUTH, handleFetch);
79 | };
80 |
81 | function* logoutCycle() {
82 | yield takeLatest(LOGOUT, handleFetch);
83 | };
84 |
85 | function* fetchCycle() {
86 | yield takeEvery(action => action.meta && action.meta.fetch && !AUTH_ACTION_TYPES.includes(action.type), handleFetch);
87 | };
88 |
89 | yield race({
90 | logoutSuccess: take(`${LOGOUT}_SUCCESS`),
91 | unauthFailure: take(action => (action.type === FETCH_FAILURE && action.payload.status === 401)),
92 | loadAuthCycle: call(loadAuthCycle),
93 | logoutCycle: call(logoutCycle),
94 | fetchCycle: call(fetchCycle),
95 | });
96 |
97 | /* Come back to the beginning of the loop */
98 | }
99 | };
100 |
101 | return appSaga;
102 | };
103 |
104 | export default createAppSaga;
--------------------------------------------------------------------------------
/website/src/APIManager/APIManager.js:
--------------------------------------------------------------------------------
1 | export const GET = 'GET';
2 | export const POST = 'POST';
3 | export const PUT = 'PUT';
4 |
5 | /**
6 | * Request manager that build API calls and handles responses from API.
7 | * @param {Func} httpClient - HTTP client that return a fetch promise base on a given url and options
8 | */
9 | const APIManager = (httpClient) => {
10 | /**
11 | * Function that computes the url to fetch and options to be used for fetching
12 | * @param {String} type - extended type of request
13 | * @param {String} APIBaseURL - url of the API to fetch data from
14 | * @param {String} resource - resource to fetch
15 | * @param {Object} params - provided parameters
16 | */
17 | const makeOptions = (type, APIBaseURL, resource, params) => {
18 | let url;
19 | const options = {};
20 |
21 | switch (type) {
22 | case GET:
23 | url = `${APIBaseURL}/${resource}`;
24 | options.method = 'GET';
25 | break;
26 |
27 | case POST:
28 | url = `${APIBaseURL}/${resource}`;
29 | options.method = 'POST';
30 | options.body = JSON.stringify(params.data);
31 | break;
32 |
33 | case PUT:
34 | url = `${APIBaseURL}/${resource}`;
35 | options.method = 'PUT';
36 | break;
37 |
38 | default:
39 | throw new Error(`Unsupported fetch action type ${type}`);
40 | }
41 |
42 | if (params && params.csrfToken) {
43 | options.csrfToken = params.csrfToken
44 | }
45 |
46 | return {url, options};
47 | };
48 |
49 | /**
50 | * Function that handles responses from API
51 | * @param {Object} response - HTTP response
52 | * @param {String} type - extended type of request
53 | * @param {String} APIBaseURL - url of the API to fetch data from
54 | * @param {String} resource - resource to fetch
55 | * @param {String} params - provided parameters
56 | */
57 | const handleResponse = (response, type, APIBaseURL, resource, params) => {
58 | switch (type) {
59 | default:
60 | return response && response.json ? response.json : undefined;
61 | }
62 | };
63 |
64 | /**
65 | * Function that handles errors from API
66 | * @param {Object} error - error
67 | * @param {String} type - extended type of request
68 | * @param {String} APIBaseURL - url of the API to fetch data from
69 | * @param {String} resource - resource to fetch
70 | * @param {String} params - provided parameters
71 | */
72 | const handleError = (error, type, APIBaseURL, resource, params) => {
73 | switch (type) {
74 | default:
75 | return Promise.reject(error);
76 | }
77 | };
78 |
79 | /**
80 | * Function that perform fetch
81 | * @param {String} type - extended type of request
82 | * @param {String} APIBaseURL - url of the API to fetch data from
83 | * @param {String} resource - resource to fetch
84 | * @param {String} params - provided parameters
85 | */
86 | const manageFetch = (type, APIBaseURL, resource, params) => {
87 | const {
88 | url,
89 | options
90 | } = makeOptions(type, APIBaseURL, resource, params);
91 | return (
92 | httpClient(url, options).then(
93 | response => handleResponse(response, type, APIBaseURL, resource, params),
94 | error => handleError(error, type, APIBaseURL, resource, params)
95 | )
96 | );
97 | };
98 |
99 | return manageFetch;
100 | };
101 |
102 | export default APIManager;
--------------------------------------------------------------------------------
/website/src/ui/containers/Login/Login.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | PropTypes
3 | } from 'react';
4 | import {
5 | connect,
6 | } from 'react-redux';
7 | import {
8 | propTypes,
9 | reduxForm,
10 | Field,
11 | } from 'redux-form';
12 | import {
13 | login,
14 | } from '../../../redux/actions';
15 | import compose from 'recompose/compose';
16 |
17 | import getMuiTheme from 'material-ui/styles/getMuiTheme';
18 | import autoprefixer from 'material-ui/utils/autoprefixer';
19 | import LockIcon from 'material-ui/svg-icons/action/lock-outline';
20 | import {
21 | Card,
22 | CardActions,
23 | } from 'material-ui/Card';
24 | import Avatar from 'material-ui/Avatar';
25 | import RaisedButton from 'material-ui/RaisedButton';
26 | import CircularProgress from 'material-ui/CircularProgress';
27 |
28 | import defaultTheme from '../defaultTheme';
29 |
30 | import TextInput from '../../components/TextInput';
31 |
32 | const styles = {
33 | body: {
34 | display: 'flex',
35 | flexDirection: 'column',
36 | minHeight: '100vh',
37 | alignItems: 'center',
38 | justifyContent: 'center',
39 | },
40 |
41 | card: {
42 | minWidth: 300,
43 | },
44 |
45 | avatar: {
46 | margin: '1em',
47 | textAlign: 'center',
48 | },
49 |
50 | form: {
51 | padding: '0 1em 1em 1em'
52 | },
53 |
54 | input: {
55 | display: 'flex',
56 | },
57 | };
58 |
59 | const prefixedStyles = {};
60 |
61 | const Login = (props) => {
62 | const {
63 | handleSubmit,
64 | onSubmit,
65 | submitting,
66 | theme
67 | } = props;
68 |
69 | const muiTheme = getMuiTheme(theme);
70 | if (!prefixedStyles.main) {
71 | const prefix = autoprefixer(muiTheme);
72 | prefixedStyles.body = prefix(styles.body);
73 | prefixedStyles.card = prefix(styles.card);
74 | prefixedStyles.avatar = prefix(styles.avatar);
75 | prefixedStyles.form = prefix(styles.form);
76 | prefixedStyles.input = prefix(styles.input);
77 | }
78 |
79 | return (
80 |
81 |
82 |
83 | } size={60} />
84 |
85 |
86 |
118 |
119 |
120 | );
121 | }
122 |
123 | Login.propTypes = {
124 | ...propTypes,
125 | theme: PropTypes.object.isRequired,
126 | csrfToken: PropTypes.string,
127 | }
128 |
129 | Login.defaultProps = {
130 | theme: defaultTheme,
131 | }
132 |
133 | const mapStateToProps = (state) => ({
134 | csrfToken: state.auth.csrfToken,
135 | });
136 |
137 | const onSubmit = (values, dispatch, props) => {
138 | dispatch(login(values, props.form, props.csrfToken))
139 | }; // when dispatching a LOGIN action it triggers a fetch saga
140 |
141 | const enhance = compose(
142 | connect(mapStateToProps),
143 | reduxForm({
144 | form: "login",
145 | onSubmit,
146 | })
147 | );
148 |
149 | export default enhance(Login);
150 |
151 |
--------------------------------------------------------------------------------
/flask-api/flask_api/resources/auth.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from flask import Blueprint, request, after_this_request, jsonify
4 | from flask_restful import Api, Resource
5 | from flask_restful.utils.cors import crossdomain
6 | from flask_security import login_user, logout_user, current_user
7 | from flask_security.forms import LoginForm
8 | from werkzeug.datastructures import MultiDict
9 | from werkzeug.exceptions import BadRequest, Unauthorized
10 | from werkzeug.wrappers import Response
11 |
12 | from ..user import _commit, user_schema
13 |
14 | from ..common import ALLOWED_CROSS_ORIGIN_DOMAIN
15 |
16 | from ..common.utils import insert_csrf_token
17 |
18 | # Convenient constants
19 | # Resources
20 | AUTH_URL_PREFIX = '/auth'
21 | LOAD_AUTH_RESOURCE = '/loadAuth'
22 | LOGIN_RESOURCE = '/login'
23 | LOGOUT_RESOURCE = '/logout'
24 |
25 | # Messages
26 | UNAUTHORIZED_ERROR_MESSAGE = "User unauthorized to access the requested resource"
27 | LOGIN_ERROR_MESSAGE = "Invalid provided credentials"
28 |
29 | # Make auth API
30 | AUTH_BLUEPRINT_NAME = 'auth'
31 | auth_bp = Blueprint(AUTH_BLUEPRINT_NAME, __name__, url_prefix=AUTH_URL_PREFIX)
32 | auth_api = Api(auth_bp)
33 |
34 |
35 | class LoadAuth(Resource):
36 | """
37 | Resource responsible for us er authentification loading
38 | """
39 | @crossdomain(origin=ALLOWED_CROSS_ORIGIN_DOMAIN, credentials=True)
40 | def get(self):
41 | if not current_user.is_authenticated:
42 | # Generate a 401 error response including a csrf token
43 | unauth_error = Unauthorized(UNAUTHORIZED_ERROR_MESSAGE)
44 | content = insert_csrf_token({'data': unauth_error.get_body()})
45 | return Response(json.dumps(content), unauth_error.code, unauth_error.get_headers())
46 | else:
47 | return jsonify(insert_csrf_token({'data': user_schema.dump(current_user).data}))
48 |
49 | # Handles preflight OPTIONS http request
50 | @crossdomain(origin=ALLOWED_CROSS_ORIGIN_DOMAIN, methods=['GET'], headers=['content-type'], credentials=True)
51 | def options(self):
52 | # When cross domain decorator is fired on OPTIONS http request a response is automatically sent
53 | # (change param automatic_options to False in order to call the function)
54 | pass
55 |
56 |
57 | class Login(Resource):
58 | """
59 | Resource responsible for login
60 | """
61 | @crossdomain(origin=ALLOWED_CROSS_ORIGIN_DOMAIN, credentials=True)
62 | def post(self):
63 | login_form = LoginForm(MultiDict(request.get_json()))
64 |
65 | if login_form.validate_on_submit():
66 | login_user(login_form.user, remember=login_form.remember.data)
67 | after_this_request(_commit)
68 | return jsonify({'data': user_schema.dump(current_user).data})
69 |
70 | # login failed
71 | login_error = BadRequest(LOGIN_ERROR_MESSAGE)
72 | return Response(json.dumps({"errors": login_form.errors, "_error": LOGIN_ERROR_MESSAGE}), login_error.code, login_error.get_headers())
73 |
74 | # Handles preflight OPTIONS http requests
75 | # Since a POST request is expected x-csrftoken header must be allowed in order to enable the main request to transmit the csrf token to the server
76 | @crossdomain(origin=ALLOWED_CROSS_ORIGIN_DOMAIN, methods=['POST'], headers=['content-type', 'x-csrftoken'], credentials=True)
77 | def options(self):
78 | # When cross domain decorator is fired on OPTIONS http request a response is automatically sent
79 | # (change param automatic_options to False in order to call the function)
80 | pass
81 |
82 |
83 | class Logout(Resource):
84 | """
85 | Resource responsible for logout
86 | """
87 | @crossdomain(origin=ALLOWED_CROSS_ORIGIN_DOMAIN, credentials=True)
88 | def put(self):
89 | if current_user.is_authenticated:
90 | logout_user()
91 | return jsonify({})
92 | else:
93 | return Unauthorized(UNAUTHORIZED_ERROR_MESSAGE).get_response()
94 |
95 | # Handles preflight OPTIONS http requests
96 | # Since a POST request is expected x-csrftoken header must be allowed in order to transmit csrf token to the server
97 | @crossdomain(origin=ALLOWED_CROSS_ORIGIN_DOMAIN, methods=['PUT'], headers=['content-type', 'x-csrftoken'], credentials=True)
98 | def options(self):
99 | # When cross domain decorator is fired on OPTIONS http request a response is automatically sent
100 | # (change param automatic_options to False in order to call the function)
101 | pass
102 |
103 | # Add ressources
104 | auth_api.add_resource(LoadAuth, LOAD_AUTH_RESOURCE)
105 | auth_api.add_resource(Login, LOGIN_RESOURCE)
106 | auth_api.add_resource(Logout, LOGOUT_RESOURCE)
107 |
--------------------------------------------------------------------------------
/website/src/redux/saga/appSaga/fetchSaga.js:
--------------------------------------------------------------------------------
1 | import {
2 | put,
3 | call,
4 | cancelled,
5 | } from 'redux-saga/effects';
6 | import {
7 | startSubmit,
8 | stopSubmit,
9 | setSubmitSucceeded,
10 | setSubmitFailed,
11 | } from 'redux-form';
12 | import {
13 | FETCH_REQUEST,
14 | FETCH_SUCCESS,
15 | FETCH_FAILURE,
16 | FETCH_CANCEL,
17 | } from '../../actions';
18 |
19 | /**
20 | * Fetch Saga creator
21 | * @param {function} APIManager - function that perform API fetch calls
22 | */
23 | const createFetchSaga = (APIManager) => {
24 | /**
25 | * Fetch saga to be run on a fetch action
26 | * @param {object} action - fetch action that triggered the saga
27 | */
28 | function* fetchSaga(action) {
29 | const {
30 | type,
31 | payload,
32 | meta: {
33 | APIBaseUrl,
34 | resource,
35 | requestType,
36 | csrfToken,
37 | formName,
38 | ...meta
39 | }
40 | } = action;
41 |
42 | /* Request dispatchs */
43 | yield [
44 | put({ type: FETCH_REQUEST }), // general request dispatch coming with every fetch action
45 | put({
46 | type: `${type}_REQUEST`,
47 | payload,
48 | meta : {
49 | date: Date.now(),
50 | ...meta,
51 | },
52 | }), // relative request dispatch for this particular fetch action
53 | formName ? put(startSubmit(formName)) : undefined, // dispatch redux-form START_SUBMIT action
54 | ];
55 |
56 | let sagaTerminated; // will hold a boolean value indicating wheter the saga has terminated or not
57 | try {
58 | /* Run the API call (this call is blocking) */
59 | const response = yield call(
60 | APIManager,
61 | requestType,
62 | APIBaseUrl,
63 | resource,
64 | {
65 | data: payload,
66 | csrfToken
67 | }
68 | );
69 |
70 | /* Indicates that the saga terminated */
71 | sagaTerminated = true;
72 |
73 | /* Success dispatch */
74 | yield [
75 | formName ? put(stopSubmit(formName)) : undefined, // dispatch redux-form STOP_SUBMIT action
76 | formName ? put(setSubmitSucceeded(formName)) : undefined, // dispatch redux-form SET_SUBMIT_SUCCEEDED action
77 | put({ type: FETCH_SUCCESS }), // general success dispatch coming with every fetch action
78 | put({
79 | type: `${type}_SUCCESS`,
80 | payload: response.data,
81 | meta: {
82 | resource,
83 | payload,
84 | date: Date.now(),
85 | csrfToken: response.csrfToken,
86 | ...meta,
87 | },
88 | }),// relative success dispatch for this particular fetch action
89 | ];
90 | } catch(error) {
91 | /* Indicates that the saga terminated */
92 | sagaTerminated = true;
93 |
94 | /* Failure dispatch */
95 | yield [
96 | formName ? put(stopSubmit(
97 | formName,
98 | error && error.message ? error.message.errors : undefined
99 | )) : undefined, // dispatch redux-form STOP_SUBMIT action
100 | formName ? put(setSubmitFailed(
101 | formName,
102 | error && error.message && error.message.errors ? Object.keys(error.message.errors) : undefined
103 | )) : undefined, // dispatch redux-form SET_SUBMIT_FAILED action
104 | put({
105 | type: FETCH_FAILURE,
106 | payload: error,
107 | error: true,
108 | }), // general failure dispatch coming with every fetch action
109 | put({
110 | type: `${type}_FAILURE`,
111 | payload: error,
112 | error: true,
113 | meta: {
114 | resource,
115 | payload,
116 | date: Date.now(),
117 | csrfToken: error.message && error.message.csrfToken,
118 | ...meta,
119 | },
120 | }), // relative failure dispatch for this particular fetch action
121 | ];
122 | } finally {
123 | /* In case the saga is cancelled before terminating */
124 | if (!sagaTerminated) {
125 | if (yield cancelled()) {
126 | yield [
127 | formName ? put(stopSubmit(formName )) : undefined,
128 | put({ type: FETCH_CANCEL }),
129 | put({
130 | type: `${type}_CANCEL`,
131 | meta: {
132 | resource,
133 | payload,
134 | ...meta,
135 | },
136 | }),
137 | ];
138 | }
139 | }
140 | }
141 | };
142 |
143 | return fetchSaga;
144 | };
145 |
146 | export default createFetchSaga;
--------------------------------------------------------------------------------
/flask-api/README.md:
--------------------------------------------------------------------------------
1 | # Flask-API
2 | This project aims to build a robust Python Flask RESTful API that efficiently and safely exposes data to the Web. It uses [Flask](https://github.com/pallets/flask) as main underlying technology. Choosing Flask can be argued by multiple reasons, main ones being
3 | - Flask is a light weighted framework that has proven its robustness and efficiency
4 | - Flask makes no assumptions on the rest of your stack so it easily integrates with any other Python librairies
5 | - It naturally fits in dockerized microservices architectures
6 | - Flask is very popular and the community is very active, making it very straight forward to skill up on
7 | - More generally, Python is amazing ! :-)
8 |
9 | ## Table of Contents
10 | 1. [Packages](#Packages)
11 | 2. [Getting Started](#getting-started)
12 | 3. [Application Structure](#application-structure)
13 | 4. [Implementation](#implementation)
14 | 5. [Testing](#testing)
15 | 6. [Deployment](#deployment)
16 |
17 |
18 | ## Packages
19 | This project covers usage of multiple libraries that facilitates creating REST APIs with Python, main ones being
20 | - [Flask](https://github.com/pallets/flask) - Python microframework for Web development.
21 | - [Flask-RESTful](https://github.com/flask-restful/flask-restful) - Flask extension that allows to easily expose REST APIs
22 | - [Flask-Login](https://github.com/maxcountryman/flask-login) - Flask extension that manages user session (login, logout, etc.)
23 | - [Flask-WTF](https://github.com/lepture/flask-wtf) - Flask extension that allows to handle forms. It also includes CSRF protection
24 | - [SQLAlchemy](https://github.com/zzzeek/sqlalchemy) - Object Relationship Mapper (ORM) that allows easy dialog with SQL databases
25 | - [Marshmallow](https://github.com/marshmallow-code/marshmallow) - Convenient package to serialize/deserialize Python objects into json format
26 | - [Flask-Script](https://github.com/smurfix/flask-script) - Convenient Flask extension that allows to implement CLI commands
27 |
28 | ## Getting Started
29 | ### Installation
30 | You can get all scripts from this project by cloning the Github repository
31 | ```bash
32 | $ git clone
33 | $ cd flask-api
34 | ```
35 |
36 | In order to run this project, we highly recommend to use a python virtual environment with a version of Python 3.4 or higher. Assuming you have a Python 3 version installed that is bound to ```python3```, you can create a virtual environment by typing
37 | ```bash
38 | $ virtualenv venv -p python3
39 | ```
40 | If ```virtualenv``` command is not recognized, you can try to install it via
41 |
42 | ```bash
43 | $ pip install virtualenv
44 | ```
45 | Once the virtual environment is created you can install all python dependencies
46 | ```bash
47 | $ . venv/bin/activate # on linux and MacOS
48 | $ pip install -r requirements.txt
49 | ```
50 | For full information concerning ```virtualenv``` please refer to the [official documentation](https://virtualenv.pypa.io/en/stable/).
51 |
52 | ### Running
53 | ***TODO : to be completed***
54 |
55 | ## Application Structure
56 | The application structure is inspired from [fbone](https://github.com/imwilsonxu/fbone)
57 | ```
58 | .
59 | ├── config/ # Flask configuration module
60 | │ ├── __init__.py # Manage exports
61 | │ ├── cfg.py # Flask and Flask extensions configurations
62 | │ └── secret_generator.py # Secret keys file generator
63 | ├── flask_api/ # Application source code
64 | │ ├── resources/ # API Resources implementation
65 | │ │ │── __init__.py # Manage exports
66 | │ │ └── auth.py # Main file for layout
67 | │ ├── user/ # User module that allow to access users' information
68 | │ │ ├── __init__.py # Manage exports
69 | │ │ ├── models.py # SQLAlchemy models (User, Role)
70 | │ │ ├── helpers.py # Convenient functions to manipulate SQLAlchemy models
71 | │ │ ├── serializers.py # Marshmallow schema to serialize User SQLAlchemy objects
72 | │ │ ├── manager.py # Flask-Script CLI commands to manipulate User database
73 | │ │ └── constants.py # Set of convenient constants
74 | │ ├── common/ # Set of elements useful for the whole application
75 | │ │ ├── __init__.py # Manage exports
76 | │ │ ├── utils.py # Functions that every-where in the application
77 | │ │ └── constants.py # Set of convenient constants
78 | │ ├── __init__.py # Main HTML page container for app
79 | │ ├── app.py # Functions that allow App instantiation
80 | │ ├── decorators.py # Set of useful decorators
81 | │ └── extensions.py # Instantiate flask extensions ()
82 | ├── run.py # Script that run the application
83 | └── manage.py # Flask-Script CLI commands to manipulate initialize databases
84 | ```
85 |
86 | ## Implementation
87 | ***TODO : to be completed***
88 |
89 | ## Testing
90 | ***TODO : to be completed and implemented***
91 |
92 | ## Deployment
93 | ***TODO : to be completed***
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/website/src/redux/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | combineReducers
3 | } from 'redux';
4 | import {
5 | LOAD_AUTH,
6 | LOGIN,
7 | LOGOUT,
8 | FETCH_ERROR,
9 | } from '../actions';
10 | /**
11 | * Reducers related to authentification handling
12 | */
13 |
14 | /* reducer responsible for auth.user management */
15 | const userReducer = (state = null, action) => {
16 | switch (action.type) {
17 | case `${LOGIN}_SUCCESS`:
18 | return action.payload;
19 |
20 | case `${LOAD_AUTH}_SUCCESS`:
21 | return action.payload;
22 |
23 | case `${LOAD_AUTH}_FAILURE`:
24 | if (action.payload.status === 401) {
25 | return null;
26 | } else {
27 | return state;
28 | }
29 |
30 | case `${LOGOUT}_SUCCESS`:
31 | return null;
32 |
33 | case `${FETCH_ERROR}_FAILURE`:
34 | if (action.payload.status === 401) {
35 | return null;
36 | } else {
37 | return state;
38 | }
39 |
40 | default:
41 | return state;
42 | }
43 | };
44 |
45 | /* reducer responsible for login status management */
46 | const loginStatusReducer = (state = null, action) => {
47 | switch (action.type) {
48 | case `${LOGIN}_REQUEST`:
49 | return {
50 | loggingIn: true,
51 | requestDate: action.meta.date,
52 | };
53 |
54 | case `${LOGIN}_SUCCESS`:
55 | return {
56 | ...state,
57 | loggingIn: false,
58 | successDate: action.meta.date,
59 | };
60 |
61 | case `${LOGIN}_FAILURE`:
62 | return {
63 | ...state,
64 | loggingIn: false,
65 | failureDate: action.meta.date,
66 | error: action.payload,
67 | };
68 |
69 | case `${LOGIN}_CANCEL`:
70 | return {
71 | ...state,
72 | loggingIn: false,
73 | cancelDate: action.meta.date,
74 | };
75 |
76 | case `${LOGOUT}_SUCCESS`:
77 | return null;
78 |
79 | case `${LOAD_AUTH}_FAILURE`:
80 | if (action.payload.status === 401) {
81 | return null;
82 | } else {
83 | return state;
84 | }
85 |
86 | default:
87 | return state;
88 | }
89 | };
90 |
91 | /* reducer responsible for load authentification status management */
92 | const loadAuthStatusReducer = (state = null, action) => {
93 | switch (action.type) {
94 | case `${LOAD_AUTH}_REQUEST`:
95 | return {
96 | loading: true,
97 | requestDate: action.meta.date,
98 | };
99 |
100 | case `${LOAD_AUTH}_SUCCESS`:
101 | return {
102 | ...state,
103 | loading: false,
104 | successDate: action.meta.date,
105 | };
106 |
107 | case `${LOAD_AUTH}_FAILURE`:
108 | return {
109 | ...state,
110 | loading: false,
111 | failureDate: action.meta.date,
112 | error: action.payload,
113 | };
114 |
115 | case `${LOAD_AUTH}_CANCEL`:
116 | return {
117 | ...state,
118 | loading: false,
119 | cancelDate: action.meta.date,
120 | };
121 |
122 | case `${LOGOUT}_SUCCESS`:
123 | return null;
124 |
125 | default:
126 | return state;
127 | }
128 | };
129 |
130 | /* reducer responsible for logout status management */
131 | const logoutStatusReducer = (state = null, action) => {
132 | switch (action.type) {
133 | case `${LOGOUT}_REQUEST`:
134 | return {
135 | loggingOut: true,
136 | requestDate: action.meta.date,
137 | };
138 |
139 | case `${LOGOUT}_SUCCESS`:
140 | return {
141 | ...state,
142 | loggingOut: false,
143 | successDate: action.meta.date,
144 | };
145 |
146 | case `${LOGOUT}_FAILURE`:
147 | return {
148 | ...state,
149 | loggingOut: false,
150 | failureDate: action.meta.date,
151 | error: action.payload,
152 | };
153 |
154 | case `${LOGOUT}_CANCEL`:
155 | return {
156 | ...state,
157 | loggingOut: false,
158 | cancelDate: action.meta.date,
159 | };
160 |
161 | case `${LOGIN}_SUCCESS`:
162 | return null;
163 |
164 | case `${LOAD_AUTH}_SUCCESS`:
165 | return null;
166 |
167 | default:
168 | return state;
169 | }
170 | };
171 |
172 | /* Reducer responsible for csrf token management */
173 | const csrfTokenReducer = (state = null, action) => {
174 | switch (action.type) {
175 | case `${LOAD_AUTH}_SUCCESS`:
176 | return action.meta.csrfToken;
177 |
178 | case `${LOAD_AUTH}_FAILURE`:
179 | return action.meta.csrfToken ? action.meta.csrfToken : state;
180 |
181 | default:
182 | return state;
183 | }
184 | };
185 |
186 | /* Combine all reducers into the auth reducers */
187 | export default combineReducers({
188 | user: userReducer,
189 | status: combineReducers({
190 | load: loadAuthStatusReducer,
191 | login: loginStatusReducer,
192 | logout: logoutStatusReducer,
193 | }),
194 | csrfToken: csrfTokenReducer,
195 | });
196 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | # React-SPA
2 | This project aims to build a Single Page Application that properly retrieve data by fecthing some REST API. It uses [React](https://github.com/facebook/react) as main underlying technology. Choosing React can be argued by multiple reasons, main ones being
3 | - React is an high performing Front End library
4 | - React makes no assumptions on the rest of your stack. In particular it can be easily combined with any Back End technology
5 | - React is Component-Based so it allows to write very clear and predictable code (this is even more true when using Redux)
6 | - React is very popular and the community is very active, making it very straight forward to skill up on
7 |
8 | ## Table of Contents
9 | 1. [Packages](#Packages)
10 | 2. [Getting Started](#getting-started)
11 | 3. [Application Structure](#application-structure)
12 | 4. [Implementation](#implementation)
13 | 5. [Testing](#testing)
14 | 6. [Deployment](#deployment)
15 |
16 | ## Packages
17 | This project covers most of what we believe as being the best React libraries
18 | - [Create-React-App](https://github.com/facebookincubator/create-react-app) - Facebook project intended to easily package React Applications
19 | - [Redux](https://github.com/reactjs/redux) - Very popular package that allows proper Application State management
20 | - [React-Router](https://github.com/ReactTraining/react-router) - Package that allows to dynamically manage Applications Route
21 | - [Redux-Saga](https://github.com/redux-saga/redux-saga) - Package that properly handles side effects (e.g. asynchronous fetch calls)
22 | - [Redux-Form](https://github.com/erikras/redux-form) - Package that allows to easily synchonize forms and Redux state
23 | - [Material-UI](https://github.com/callemall/material-ui) - Library of React components that implements *Google Material Design* specification
24 |
25 | ## Getting Started
26 |
27 | ### Requirements
28 | ***TODO : to be completed***
29 |
30 | ### Installation
31 | You can get all scripts from this project by cloning the Github repository
32 | ```bash
33 | $ git clone
34 | $ cd
35 | ```
36 | Once the repository is cloned you only need to install all the dependencies for this project
37 | ```bash
38 | $ npm install
39 | ```
40 | You are ready go !
41 |
42 | ### Running
43 | ***TODO : to be completed***
44 |
45 | ## Application Structure
46 | The application structure is inspired from [fbone](https://github.com/imwilsonxu/fbone)
47 | ```
48 | .
49 | ├── config # Flask configuration module
50 | │ ├── __init__.py # Manage exports
51 | │ ├── cfg.py # Flask and Flask extensions configurations
52 | │ └── secret_generator.py # Secret keys file generator
53 | ├── flask_api # Application source code
54 | │ ├── resources # API Resources implementation
55 | │ │ │── __init__.py # Manage exports
56 | │ │ └── auth.py # Main file for layout
57 | │ ├── user # User module that allow to access users' information
58 | │ │ ├── __init__.py # Manage exports
59 | │ │ ├── models.py # SQLAlchemy models (User, Role)
60 | │ │ ├── helpers.py # Convenient functions to manipulate SQLAlchemy models
61 | │ │ ├── serializers.py # Marshmallow schema to serialize User SQLAlchemy objects
62 | │ │ ├── manager.py # Flask-Script CLI commands to manipulate User database
63 | │ │ └── constants.py # Set of convenient constants
64 | │ ├── common # Set of elements useful for the whole application
65 | │ │ ├── __init__.py # Manage exports
66 | │ │ ├── utils.py # Functions that every-where in the application
67 | │ │ └── constants.py # Set of convenient constants
68 | │ ├── __init__.py # Main HTML page container for app
69 | │ ├── app.py # Functions that allow App instantiation
70 | │ ├── decorators.py # Set of useful decorators
71 | │ └── extensions.py # Instantiate flask extensions ()
72 | ├── run.py # Script that run the application
73 | └── manage.py # Flask-Script CLI commands to manipulate initialize databases
74 | ```
75 |
76 | ## Implementation
77 | ***TODO : to be completed***
78 |
79 | ## Testing
80 | ***TODO : to be completed and implemented***
81 |
82 | ## Deployment
83 | ***TODO : to be completed***
84 |
85 | #
86 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
87 |
88 | Below you will find some information on how to perform common tasks.
89 | You can find the most recent version of this guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md).
90 |
91 | ## Table of Contents
92 |
93 | - [Updating to New Releases](#updating-to-new-releases)
94 | - [Sending Feedback](#sending-feedback)
95 | - [Folder Structure](#folder-structure)
96 | - [Available Scripts](#available-scripts)
97 | - [npm start](#npm-start)
98 | - [npm test](#npm-test)
99 | - [npm run build](#npm-run-build)
100 | - [npm run eject](#npm-run-eject)
101 | - [Supported Language Features and Polyfills](#supported-language-features-and-polyfills)
102 | - [Syntax Highlighting in the Editor](#syntax-highlighting-in-the-editor)
103 | - [Displaying Lint Output in the Editor](#displaying-lint-output-in-the-editor)
104 | - [Debugging in the Editor](#debugging-in-the-editor)
105 | - [Changing the Page ``](#changing-the-page-title)
106 | - [Installing a Dependency](#installing-a-dependency)
107 | - [Importing a Component](#importing-a-component)
108 | - [Adding a Stylesheet](#adding-a-stylesheet)
109 | - [Post-Processing CSS](#post-processing-css)
110 | - [Adding a CSS Preprocessor (Sass, Less etc.)](#adding-a-css-preprocessor-sass-less-etc)
111 | - [Adding Images and Fonts](#adding-images-and-fonts)
112 | - [Using the `public` Folder](#using-the-public-folder)
113 | - [Changing the HTML](#changing-the-html)
114 | - [Adding Assets Outside of the Module System](#adding-assets-outside-of-the-module-system)
115 | - [When to Use the `public` Folder](#when-to-use-the-public-folder)
116 | - [Using Global Variables](#using-global-variables)
117 | - [Adding Bootstrap](#adding-bootstrap)
118 | - [Using a Custom Theme](#using-a-custom-theme)
119 | - [Adding Flow](#adding-flow)
120 | - [Adding Custom Environment Variables](#adding-custom-environment-variables)
121 | - [Referencing Environment Variables in the HTML](#referencing-environment-variables-in-the-html)
122 | - [Adding Temporary Environment Variables In Your Shell](#adding-temporary-environment-variables-in-your-shell)
123 | - [Adding Development Environment Variables In `.env`](#adding-development-environment-variables-in-env)
124 | - [Can I Use Decorators?](#can-i-use-decorators)
125 | - [Integrating with an API Backend](#integrating-with-an-api-backend)
126 | - [Node](#node)
127 | - [Ruby on Rails](#ruby-on-rails)
128 | - [Proxying API Requests in Development](#proxying-api-requests-in-development)
129 | - [Using HTTPS in Development](#using-https-in-development)
130 | - [Generating Dynamic `` Tags on the Server](#generating-dynamic-meta-tags-on-the-server)
131 | - [Pre-Rendering into Static HTML Files](#pre-rendering-into-static-html-files)
132 | - [Injecting Data from the Server into the Page](#injecting-data-from-the-server-into-the-page)
133 | - [Running Tests](#running-tests)
134 | - [Filename Conventions](#filename-conventions)
135 | - [Command Line Interface](#command-line-interface)
136 | - [Version Control Integration](#version-control-integration)
137 | - [Writing Tests](#writing-tests)
138 | - [Testing Components](#testing-components)
139 | - [Using Third Party Assertion Libraries](#using-third-party-assertion-libraries)
140 | - [Initializing Test Environment](#initializing-test-environment)
141 | - [Focusing and Excluding Tests](#focusing-and-excluding-tests)
142 | - [Coverage Reporting](#coverage-reporting)
143 | - [Continuous Integration](#continuous-integration)
144 | - [Disabling jsdom](#disabling-jsdom)
145 | - [Snapshot Testing](#snapshot-testing)
146 | - [Editor Integration](#editor-integration)
147 | - [Developing Components in Isolation](#developing-components-in-isolation)
148 | - [Making a Progressive Web App](#making-a-progressive-web-app)
149 | - [Deployment](#deployment)
150 | - [Static Server](#static-server)
151 | - [Other Solutions](#other-solutions)
152 | - [Serving Apps with Client-Side Routing](#serving-apps-with-client-side-routing)
153 | - [Building for Relative Paths](#building-for-relative-paths)
154 | - [Azure](#azure)
155 | - [Firebase](#firebase)
156 | - [GitHub Pages](#github-pages)
157 | - [Heroku](#heroku)
158 | - [Modulus](#modulus)
159 | - [Netlify](#netlify)
160 | - [Now](#now)
161 | - [S3 and CloudFront](#s3-and-cloudfront)
162 | - [Surge](#surge)
163 | - [Advanced Configuration](#advanced-configuration)
164 | - [Troubleshooting](#troubleshooting)
165 | - [`npm start` doesn’t detect changes](#npm-start-doesnt-detect-changes)
166 | - [`npm test` hangs on macOS Sierra](#npm-test-hangs-on-macos-sierra)
167 | - [`npm run build` silently fails](#npm-run-build-silently-fails)
168 | - [`npm run build` fails on Heroku](#npm-run-build-fails-on-heroku)
169 | - [Something Missing?](#something-missing)
170 |
171 | ## Updating to New Releases
172 |
173 | Create React App is divided into two packages:
174 |
175 | * `create-react-app` is a global command-line utility that you use to create new projects.
176 | * `react-scripts` is a development dependency in the generated projects (including this one).
177 |
178 | You almost never need to update `create-react-app` itself: it delegates all the setup to `react-scripts`.
179 |
180 | When you run `create-react-app`, it always creates the project with the latest version of `react-scripts` so you’ll get all the new features and improvements in newly created apps automatically.
181 |
182 | To update an existing project to a new version of `react-scripts`, [open the changelog](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md), find the version you’re currently on (check `package.json` in this folder if you’re not sure), and apply the migration instructions for the newer versions.
183 |
184 | In most cases bumping the `react-scripts` version in `package.json` and running `npm install` in this folder should be enough, but it’s good to consult the [changelog](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md) for potential breaking changes.
185 |
186 | We commit to keeping the breaking changes minimal so you can upgrade `react-scripts` painlessly.
187 |
188 | ## Sending Feedback
189 |
190 | We are always open to [your feedback](https://github.com/facebookincubator/create-react-app/issues).
191 |
192 | ## Folder Structure
193 |
194 | After creation, your project should look like this:
195 |
196 | ```
197 | my-app/
198 | README.md
199 | node_modules/
200 | package.json
201 | public/
202 | index.html
203 | favicon.ico
204 | src/
205 | App.css
206 | App.js
207 | App.test.js
208 | index.css
209 | index.js
210 | logo.svg
211 | ```
212 |
213 | For the project to build, **these files must exist with exact filenames**:
214 |
215 | * `public/index.html` is the page template;
216 | * `src/index.js` is the JavaScript entry point.
217 |
218 | You can delete or rename the other files.
219 |
220 | You may create subdirectories inside `src`. For faster rebuilds, only files inside `src` are processed by Webpack.
221 | You need to **put any JS and CSS files inside `src`**, or Webpack won’t see them.
222 |
223 | Only files inside `public` can be used from `public/index.html`.
224 | Read instructions below for using assets from JavaScript and HTML.
225 |
226 | You can, however, create more top-level directories.
227 | They will not be included in the production build so you can use them for things like documentation.
228 |
229 | ## Available Scripts
230 |
231 | In the project directory, you can run:
232 |
233 | ### `npm start`
234 |
235 | Runs the app in the development mode.
236 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
237 |
238 | The page will reload if you make edits.
239 | You will also see any lint errors in the console.
240 |
241 | ### `npm test`
242 |
243 | Launches the test runner in the interactive watch mode.
244 | See the section about [running tests](#running-tests) for more information.
245 |
246 | ### `npm run build`
247 |
248 | Builds the app for production to the `build` folder.
249 | It correctly bundles React in production mode and optimizes the build for the best performance.
250 |
251 | The build is minified and the filenames include the hashes.
252 | Your app is ready to be deployed!
253 |
254 | See the section about [deployment](#deployment) for more information.
255 |
256 | ### `npm run eject`
257 |
258 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
259 |
260 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
261 |
262 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
263 |
264 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
265 |
266 | ## Supported Language Features and Polyfills
267 |
268 | This project supports a superset of the latest JavaScript standard.
269 | In addition to [ES6](https://github.com/lukehoban/es6features) syntax features, it also supports:
270 |
271 | * [Exponentiation Operator](https://github.com/rwaldron/exponentiation-operator) (ES2016).
272 | * [Async/await](https://github.com/tc39/ecmascript-asyncawait) (ES2017).
273 | * [Object Rest/Spread Properties](https://github.com/sebmarkbage/ecmascript-rest-spread) (stage 3 proposal).
274 | * [Class Fields and Static Properties](https://github.com/tc39/proposal-class-public-fields) (stage 2 proposal).
275 | * [JSX](https://facebook.github.io/react/docs/introducing-jsx.html) and [Flow](https://flowtype.org/) syntax.
276 |
277 | Learn more about [different proposal stages](https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-).
278 |
279 | While we recommend to use experimental proposals with some caution, Facebook heavily uses these features in the product code, so we intend to provide [codemods](https://medium.com/@cpojer/effective-javascript-codemods-5a6686bb46fb) if any of these proposals change in the future.
280 |
281 | Note that **the project only includes a few ES6 [polyfills](https://en.wikipedia.org/wiki/Polyfill)**:
282 |
283 | * [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) via [`object-assign`](https://github.com/sindresorhus/object-assign).
284 | * [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) via [`promise`](https://github.com/then/promise).
285 | * [`fetch()`](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) via [`whatwg-fetch`](https://github.com/github/fetch).
286 |
287 | If you use any other ES6+ features that need **runtime support** (such as `Array.from()` or `Symbol`), make sure you are including the appropriate polyfills manually, or that the browsers you are targeting already support them.
288 |
289 | ## Syntax Highlighting in the Editor
290 |
291 | To configure the syntax highlighting in your favorite text editor, head to the [relevant Babel documentation page](https://babeljs.io/docs/editors) and follow the instructions. Some of the most popular editors are covered.
292 |
293 | ## Displaying Lint Output in the Editor
294 |
295 | >Note: this feature is available with `react-scripts@0.2.0` and higher.
296 |
297 | Some editors, including Sublime Text, Atom, and Visual Studio Code, provide plugins for ESLint.
298 |
299 | They are not required for linting. You should see the linter output right in your terminal as well as the browser console. However, if you prefer the lint results to appear right in your editor, there are some extra steps you can do.
300 |
301 | You would need to install an ESLint plugin for your editor first.
302 |
303 | >**A note for Atom `linter-eslint` users**
304 |
305 | >If you are using the Atom `linter-eslint` plugin, make sure that **Use global ESLint installation** option is checked:
306 |
307 | >
308 |
309 |
310 | >**For Visual Studio Code users**
311 |
312 | >VS Code ESLint plugin automatically detects Create React App's configuration file. So you do not need to create `eslintrc.json` at the root directory, except when you want to add your own rules. In that case, you should include CRA's config by adding this line:
313 |
314 | >```js
315 | {
316 | // ...
317 | "extends": "react-app"
318 | }
319 | ```
320 |
321 | Then add this block to the `package.json` file of your project:
322 |
323 | ```js
324 | {
325 | // ...
326 | "eslintConfig": {
327 | "extends": "react-app"
328 | }
329 | }
330 | ```
331 |
332 | Finally, you will need to install some packages *globally*:
333 |
334 | ```sh
335 | npm install -g eslint-config-react-app@0.3.0 eslint@3.8.1 babel-eslint@7.0.0 eslint-plugin-react@6.4.1 eslint-plugin-import@2.0.1 eslint-plugin-jsx-a11y@4.0.0 eslint-plugin-flowtype@2.21.0
336 | ```
337 |
338 | We recognize that this is suboptimal, but it is currently required due to the way we hide the ESLint dependency. The ESLint team is already [working on a solution to this](https://github.com/eslint/eslint/issues/3458) so this may become unnecessary in a couple of months.
339 |
340 | ## Debugging in the Editor
341 |
342 | **This feature is currently only supported by [Visual Studio Code](https://code.visualstudio.com) editor.**
343 |
344 | Visual Studio Code supports live-editing and debugging out of the box with Create React App. This enables you as a developer to write and debug your React code without leaving the editor, and most importantly it enables you to have a continuous development workflow, where context switching is minimal, as you don’t have to switch between tools.
345 |
346 | You would need to have the latest version of [VS Code](https://code.visualstudio.com) and VS Code [Chrome Debugger Extension](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) installed.
347 |
348 | Then add the block below to your `launch.json` file and put it inside the `.vscode` folder in your app’s root directory.
349 |
350 | ```json
351 | {
352 | "version": "0.2.0",
353 | "configurations": [{
354 | "name": "Chrome",
355 | "type": "chrome",
356 | "request": "launch",
357 | "url": "http://localhost:3000",
358 | "webRoot": "${workspaceRoot}/src",
359 | "userDataDir": "${workspaceRoot}/.vscode/chrome",
360 | "sourceMapPathOverrides": {
361 | "webpack:///src/*": "${webRoot}/*"
362 | }
363 | }]
364 | }
365 | ```
366 |
367 | Start your app by running `npm start`, and start debugging in VS Code by pressing `F5` or by clicking the green debug icon. You can now write code, set breakpoints, make changes to the code, and debug your newly modified code—all from your editor.
368 |
369 | ## Changing the Page ``
370 |
371 | You can find the source HTML file in the `public` folder of the generated project. You may edit the `` tag in it to change the title from “React App” to anything else.
372 |
373 | Note that normally you wouldn’t edit files in the `public` folder very often. For example, [adding a stylesheet](#adding-a-stylesheet) is done without touching the HTML.
374 |
375 | If you need to dynamically update the page title based on the content, you can use the browser [`document.title`](https://developer.mozilla.org/en-US/docs/Web/API/Document/title) API. For more complex scenarios when you want to change the title from React components, you can use [React Helmet](https://github.com/nfl/react-helmet), a third party library.
376 |
377 | If you use a custom server for your app in production and want to modify the title before it gets sent to the browser, you can follow advice in [this section](#generating-dynamic-meta-tags-on-the-server). Alternatively, you can pre-build each page as a static HTML file which then loads the JavaScript bundle, which is covered [here](#pre-rendering-into-static-html-files).
378 |
379 | ## Installing a Dependency
380 |
381 | The generated project includes React and ReactDOM as dependencies. It also includes a set of scripts used by Create React App as a development dependency. You may install other dependencies (for example, React Router) with `npm`:
382 |
383 | ```
384 | npm install --save
385 | ```
386 |
387 | ## Importing a Component
388 |
389 | This project setup supports ES6 modules thanks to Babel.
390 | While you can still use `require()` and `module.exports`, we encourage you to use [`import` and `export`](http://exploringjs.com/es6/ch_modules.html) instead.
391 |
392 | For example:
393 |
394 | ### `Button.js`
395 |
396 | ```js
397 | import React, { Component } from 'react';
398 |
399 | class Button extends Component {
400 | render() {
401 | // ...
402 | }
403 | }
404 |
405 | export default Button; // Don’t forget to use export default!
406 | ```
407 |
408 | ### `DangerButton.js`
409 |
410 |
411 | ```js
412 | import React, { Component } from 'react';
413 | import Button from './Button'; // Import a component from another file
414 |
415 | class DangerButton extends Component {
416 | render() {
417 | return ;
418 | }
419 | }
420 |
421 | export default DangerButton;
422 | ```
423 |
424 | Be aware of the [difference between default and named exports](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281). It is a common source of mistakes.
425 |
426 | We suggest that you stick to using default imports and exports when a module only exports a single thing (for example, a component). That’s what you get when you use `export default Button` and `import Button from './Button'`.
427 |
428 | Named exports are useful for utility modules that export several functions. A module may have at most one default export and as many named exports as you like.
429 |
430 | Learn more about ES6 modules:
431 |
432 | * [When to use the curly braces?](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281)
433 | * [Exploring ES6: Modules](http://exploringjs.com/es6/ch_modules.html)
434 | * [Understanding ES6: Modules](https://leanpub.com/understandinges6/read#leanpub-auto-encapsulating-code-with-modules)
435 |
436 | ## Adding a Stylesheet
437 |
438 | This project setup uses [Webpack](https://webpack.github.io/) for handling all assets. Webpack offers a custom way of “extending” the concept of `import` beyond JavaScript. To express that a JavaScript file depends on a CSS file, you need to **import the CSS from the JavaScript file**:
439 |
440 | ### `Button.css`
441 |
442 | ```css
443 | .Button {
444 | padding: 20px;
445 | }
446 | ```
447 |
448 | ### `Button.js`
449 |
450 | ```js
451 | import React, { Component } from 'react';
452 | import './Button.css'; // Tell Webpack that Button.js uses these styles
453 |
454 | class Button extends Component {
455 | render() {
456 | // You can use them as regular CSS styles
457 | return ;
458 | }
459 | }
460 | ```
461 |
462 | **This is not required for React** but many people find this feature convenient. You can read about the benefits of this approach [here](https://medium.com/seek-ui-engineering/block-element-modifying-your-javascript-components-d7f99fcab52b). However you should be aware that this makes your code less portable to other build tools and environments than Webpack.
463 |
464 | In development, expressing dependencies this way allows your styles to be reloaded on the fly as you edit them. In production, all CSS files will be concatenated into a single minified `.css` file in the build output.
465 |
466 | If you are concerned about using Webpack-specific semantics, you can put all your CSS right into `src/index.css`. It would still be imported from `src/index.js`, but you could always remove that import if you later migrate to a different build tool.
467 |
468 | ## Post-Processing CSS
469 |
470 | This project setup minifies your CSS and adds vendor prefixes to it automatically through [Autoprefixer](https://github.com/postcss/autoprefixer) so you don’t need to worry about it.
471 |
472 | For example, this:
473 |
474 | ```css
475 | .App {
476 | display: flex;
477 | flex-direction: row;
478 | align-items: center;
479 | }
480 | ```
481 |
482 | becomes this:
483 |
484 | ```css
485 | .App {
486 | display: -webkit-box;
487 | display: -ms-flexbox;
488 | display: flex;
489 | -webkit-box-orient: horizontal;
490 | -webkit-box-direction: normal;
491 | -ms-flex-direction: row;
492 | flex-direction: row;
493 | -webkit-box-align: center;
494 | -ms-flex-align: center;
495 | align-items: center;
496 | }
497 | ```
498 |
499 | If you need to disable autoprefixing for some reason, [follow this section](https://github.com/postcss/autoprefixer#disabling).
500 |
501 | ## Adding a CSS Preprocessor (Sass, Less etc.)
502 |
503 | Generally, we recommend that you don’t reuse the same CSS classes across different components. For example, instead of using a `.Button` CSS class in `` and `` components, we recommend creating a `