├── __init__.py
├── application
├── __init__.py
├── utils
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── auth.cpython-36.pyc
│ │ └── __init__.cpython-36.pyc
│ └── auth.py
├── __pycache__
│ ├── app.cpython-36.pyc
│ ├── __init__.cpython-36.pyc
│ └── models.cpython-36.pyc
├── models.py
└── app.py
├── runtime.txt
├── Procfile
├── migrations
├── README
├── __pycache__
│ └── env.cpython-36.pyc
├── versions
│ ├── __pycache__
│ │ ├── 41c073a46b63_.cpython-36.pyc
│ │ ├── 5aae5ada6624_.cpython-36.pyc
│ │ ├── a7bca515a457_.cpython-36.pyc
│ │ └── ed657e16ce20_.cpython-36.pyc
│ ├── 5aae5ada6624_.py
│ ├── a7bca515a457_.py
│ ├── ed657e16ce20_.py
│ └── 41c073a46b63_.py
├── script.py.mako
├── alembic.ini
└── env.py
├── main.py
├── react-ui
├── src
│ ├── style.scss
│ ├── index.css
│ ├── containers
│ │ ├── App
│ │ │ ├── styles
│ │ │ │ ├── index.js
│ │ │ │ ├── links.scss
│ │ │ │ ├── fonts
│ │ │ │ │ └── roboto.scss
│ │ │ │ ├── typography.scss
│ │ │ │ ├── app.scss
│ │ │ │ └── screens.scss
│ │ │ └── index.js
│ │ └── HomeContainer
│ │ │ └── index.js
│ ├── components
│ │ ├── Home
│ │ │ └── index.js
│ │ ├── Footer
│ │ │ ├── styles.scss
│ │ │ └── index.js
│ │ ├── Analytics.js
│ │ ├── NotFound.js
│ │ ├── ProtectedView.js
│ │ ├── DetermineAuth.js
│ │ ├── Header
│ │ │ └── index.js
│ │ ├── RegisterView.js
│ │ └── LoginView.js
│ ├── utils
│ │ ├── isMobileAndTablet.js
│ │ ├── parallax.js
│ │ ├── checkAuth.js
│ │ ├── misc.js
│ │ └── http_functions.js
│ ├── App.test.js
│ ├── index.js
│ ├── reducers
│ │ ├── index.js
│ │ ├── data.js
│ │ └── auth.js
│ ├── App.css
│ ├── constants
│ │ └── index.js
│ ├── actions
│ │ ├── data.js
│ │ └── auth.js
│ ├── store
│ │ └── configureStore.js
│ ├── App.js
│ ├── routes.js
│ └── registerServiceWorker.js
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── index.html
├── .eslintrc
├── package.json
└── bootstrap.rc
├── basedir.py
├── setup.py
├── __pycache__
├── index.cpython-36.pyc
├── setup.cpython-36.pyc
├── basedir.cpython-36.pyc
├── config.cpython-36.pyc
└── testing_config.cpython-36.pyc
├── tests
├── __pycache__
│ ├── test_api.cpython-36-PYTEST.pyc
│ └── test_models.cpython-36-PYTEST.pyc
├── test_models.py
└── test_api.py
├── index.py
├── .gitignore
├── manage.py
├── config.py
├── requirements.txt
├── test.py
├── testing_config.py
├── LICENSE
└── README.md
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/application/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/runtime.txt:
--------------------------------------------------------------------------------
1 | python-2.7.9
2 |
--------------------------------------------------------------------------------
/application/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn main:app
2 |
--------------------------------------------------------------------------------
/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from application.app import app
2 |
3 | app = app
4 |
--------------------------------------------------------------------------------
/react-ui/src/style.scss:
--------------------------------------------------------------------------------
1 | :global(body) {
2 | position: relative;
3 | }
4 |
--------------------------------------------------------------------------------
/basedir.py:
--------------------------------------------------------------------------------
1 | import os
2 | basedir = os.path.abspath(os.path.dirname(__file__))
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | basedir = os.path.abspath(os.path.dirname(__file__))
3 |
--------------------------------------------------------------------------------
/react-ui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/react-ui/public/favicon.ico
--------------------------------------------------------------------------------
/react-ui/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/__pycache__/index.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/__pycache__/index.cpython-36.pyc
--------------------------------------------------------------------------------
/__pycache__/setup.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/__pycache__/setup.cpython-36.pyc
--------------------------------------------------------------------------------
/__pycache__/basedir.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/__pycache__/basedir.cpython-36.pyc
--------------------------------------------------------------------------------
/__pycache__/config.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/__pycache__/config.cpython-36.pyc
--------------------------------------------------------------------------------
/__pycache__/testing_config.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/__pycache__/testing_config.cpython-36.pyc
--------------------------------------------------------------------------------
/application/__pycache__/app.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/application/__pycache__/app.cpython-36.pyc
--------------------------------------------------------------------------------
/migrations/__pycache__/env.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/migrations/__pycache__/env.cpython-36.pyc
--------------------------------------------------------------------------------
/react-ui/src/containers/App/styles/index.js:
--------------------------------------------------------------------------------
1 | import 'style!./styles.scss';
2 |
3 | export default require('./styles.scss').locals.styles;
4 |
--------------------------------------------------------------------------------
/react-ui/src/containers/App/styles/links.scss:
--------------------------------------------------------------------------------
1 | a {
2 | text-decoration: none;
3 |
4 | &:hover {
5 | text-decoration: none;
6 | }
7 | }
--------------------------------------------------------------------------------
/application/__pycache__/__init__.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/application/__pycache__/__init__.cpython-36.pyc
--------------------------------------------------------------------------------
/application/__pycache__/models.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/application/__pycache__/models.cpython-36.pyc
--------------------------------------------------------------------------------
/application/utils/__pycache__/auth.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/application/utils/__pycache__/auth.cpython-36.pyc
--------------------------------------------------------------------------------
/tests/__pycache__/test_api.cpython-36-PYTEST.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/tests/__pycache__/test_api.cpython-36-PYTEST.pyc
--------------------------------------------------------------------------------
/tests/__pycache__/test_models.cpython-36-PYTEST.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/tests/__pycache__/test_models.cpython-36-PYTEST.pyc
--------------------------------------------------------------------------------
/application/utils/__pycache__/__init__.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/application/utils/__pycache__/__init__.cpython-36.pyc
--------------------------------------------------------------------------------
/migrations/versions/__pycache__/41c073a46b63_.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/migrations/versions/__pycache__/41c073a46b63_.cpython-36.pyc
--------------------------------------------------------------------------------
/migrations/versions/__pycache__/5aae5ada6624_.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/migrations/versions/__pycache__/5aae5ada6624_.cpython-36.pyc
--------------------------------------------------------------------------------
/migrations/versions/__pycache__/a7bca515a457_.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/migrations/versions/__pycache__/a7bca515a457_.cpython-36.pyc
--------------------------------------------------------------------------------
/migrations/versions/__pycache__/ed657e16ce20_.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blackfry/cra-flask/HEAD/migrations/versions/__pycache__/ed657e16ce20_.cpython-36.pyc
--------------------------------------------------------------------------------
/react-ui/src/containers/App/styles/fonts/roboto.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Fonts
3 | */
4 |
5 | @import url(//fonts.googleapis.com/css?family=Roboto:400,100,300,500,700,900&subset=latin,cyrillic-ext);
6 |
--------------------------------------------------------------------------------
/react-ui/src/components/Home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Home = () => (
4 |
9 | );
10 |
--------------------------------------------------------------------------------
/react-ui/src/utils/isMobileAndTablet.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Is mobile or tablet?
3 | *
4 | * @return {Boolean}
5 | */
6 | export const isMobileAndTablet = () => window.innerWidth <= 800 && window.innerHeight <= 600;
7 |
8 | export default isMobileAndTablet;
9 |
--------------------------------------------------------------------------------
/react-ui/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/react-ui/src/containers/HomeContainer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /* components */
4 | import { Home } from '../../components/Home';
5 |
6 | export const HomeContainer = () => (
7 |
10 | );
11 |
--------------------------------------------------------------------------------
/react-ui/src/components/Footer/styles.scss:
--------------------------------------------------------------------------------
1 | :local(.styles) {
2 | padding-top: 35px;
3 | padding-bottom: 30px;
4 | text-align: center;
5 | background-color: #E0F2F1;
6 | color: black;
7 | position: fixed;
8 | bottom: 0;
9 | width: 100%;
10 | }
11 |
--------------------------------------------------------------------------------
/react-ui/src/containers/App/styles/typography.scss:
--------------------------------------------------------------------------------
1 | /* Typography */
2 |
3 | body {
4 | font-family: 'Roboto', sans-serif;
5 | font-weight: 300;
6 | }
7 |
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6 {
14 | font-weight: 300;
15 | }
16 |
17 | p {
18 | font-size: 16px;
19 | }
--------------------------------------------------------------------------------
/react-ui/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/react-ui/src/containers/App/styles/app.scss:
--------------------------------------------------------------------------------
1 | /* global styles */
2 | @import 'fonts/roboto';
3 | @import 'typography';
4 | @import 'links';
5 |
6 | :global(body) {
7 | position: relative;
8 | font-family: 'Roboto', sans-serif !important;
9 | h1, h2, h3, h4 {
10 | font-weight: 300;
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/index.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from flask_sqlalchemy import SQLAlchemy
3 | from config import BaseConfig
4 | from flask_bcrypt import Bcrypt
5 |
6 | app = Flask(__name__, static_folder="./static/dist", template_folder="./static")
7 | app.config.from_object(BaseConfig)
8 | db = SQLAlchemy(app)
9 | bcrypt = Bcrypt(app)
10 |
--------------------------------------------------------------------------------
/react-ui/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerReducer } from 'react-router-redux';
3 | import auth from './auth';
4 | import data from './data';
5 |
6 | const rootReducer = combineReducers({
7 | routing: routerReducer,
8 | /* your reducers */
9 | auth,
10 | data
11 | });
12 |
13 | export default rootReducer;
14 |
--------------------------------------------------------------------------------
/react-ui/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from testing_config import BaseTestConfig
2 | from application.models import User
3 |
4 |
5 | class TestModels(BaseTestConfig):
6 | def test_get_user_with_email_and_password(self):
7 | self.assertTrue(
8 | User.get_user_with_email_and_password(
9 | self.default_user["email"],
10 | self.default_user["password"])
11 | )
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /react-ui/node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 |
24 | /.vscode
25 | /__pycache__
26 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | from flask_script import Manager
2 | from flask_migrate import Migrate, MigrateCommand
3 |
4 | from application.app import app, db
5 |
6 | migrate = Migrate(app, db)
7 | manager = Manager(app)
8 |
9 | # migrations
10 | manager.add_command('db', MigrateCommand)
11 |
12 |
13 | @manager.command
14 | def create_db():
15 | """Creates the db tables."""
16 | db.create_all()
17 |
18 |
19 | if __name__ == '__main__':
20 | manager.run()
21 |
--------------------------------------------------------------------------------
/react-ui/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-intro {
18 | font-size: large;
19 | }
20 |
21 | @keyframes App-logo-spin {
22 | from { transform: rotate(0deg); }
23 | to { transform: rotate(360deg); }
24 | }
25 |
--------------------------------------------------------------------------------
/react-ui/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /* component styles */
4 | import { styles } from './styles.scss';
5 |
6 | const Footer = () => (
7 |
16 | );
17 |
18 | export default Footer;
19 |
--------------------------------------------------------------------------------
/react-ui/src/utils/parallax.js:
--------------------------------------------------------------------------------
1 | import { isMobileAndTablet } from './isMobileAndTablet';
2 |
3 | /*
4 | * Add parallax effect to element
5 | *
6 | * @param {Object} DOM element
7 | * @param {Integer} Animation speed, default: 30
8 | */
9 | export function setParallax(elem, speed = 30) {
10 | const top = (window.pageYOffset - elem.offsetTop) / speed;
11 |
12 | isMobileAndTablet
13 | ? (elem.style.backgroundPosition = `0px ${top}px`) // eslint-disable-line no-param-reassign
14 | : null;
15 | }
16 |
--------------------------------------------------------------------------------
/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision}
5 | Create Date: ${create_date}
6 |
7 | """
8 |
9 | # revision identifiers, used by Alembic.
10 | revision = ${repr(up_revision)}
11 | down_revision = ${repr(down_revision)}
12 |
13 | from alembic import op
14 | import sqlalchemy as sa
15 | ${imports if imports else ""}
16 |
17 | def upgrade():
18 | ${upgrades if upgrades else "pass"}
19 |
20 |
21 | def downgrade():
22 | ${downgrades if downgrades else "pass"}
23 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from setup import basedir
4 |
5 |
6 | class BaseConfig(object):
7 | SECRET_KEY = "SO_SECURE"
8 | DEBUG = True
9 | SQLALCHEMY_DATABASE_URI = 'postgresql:///mydatabase'
10 | SQLALCHEMY_TRACK_MODIFICATIONS = True
11 |
12 |
13 | class TestingConfig(object):
14 | """Development configuration."""
15 | TESTING = True
16 | DEBUG = True
17 | WTF_CSRF_ENABLED = False
18 | SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
19 | DEBUG_TB_ENABLED = True
20 | PRESERVE_CONTEXT_ON_EXCEPTION = False
21 |
--------------------------------------------------------------------------------
/react-ui/src/containers/App/styles/screens.scss:
--------------------------------------------------------------------------------
1 | // Phone
2 | //
3 | // @media (#{$phone}) {
4 | // code-here
5 | // }
6 | //
7 | $phone: "max-width: 768px";
8 |
9 |
10 | // Tablet
11 | //
12 | // @media (#{$tablet}) {
13 | // code-here
14 | // }
15 | //
16 | $tablet: "min-width: 768px";
17 |
18 |
19 | // Desktop
20 | //
21 | // @media (#{$desktop}) {
22 | // code-here
23 | // }
24 | $desktop: "min-width: 992px";
25 |
26 |
27 | // Phone
28 | //
29 | // @media (#{$large-desktop}) {
30 | // code-here
31 | // }
32 | $large-desktop: "min-width: 1200px";
--------------------------------------------------------------------------------
/react-ui/src/utils/checkAuth.js:
--------------------------------------------------------------------------------
1 | export const checkAuth = async () => {
2 | try {
3 | const token = localStorage.getItem('token');
4 | if (token) {
5 | return await fetch('api/is_token_valid', {
6 | method: 'post',
7 | credentials: 'include',
8 | headers: {
9 | Accept: 'application/json', // eslint-disable-line quote-props
10 | 'Content-Type': 'application/json'
11 | },
12 | body: JSON.stringify({ token })
13 | });
14 | }
15 | } catch (error) {
16 | return null;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/react-ui/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export const LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';
2 | export const LOGIN_USER_FAILURE = 'LOGIN_USER_FAILURE';
3 | export const LOGIN_USER_REQUEST = 'LOGIN_USER_REQUEST';
4 | export const LOGOUT_USER = 'LOGOUT_USER';
5 |
6 | export const REGISTER_USER_SUCCESS = 'REGISTER_USER_SUCCESS';
7 | export const REGISTER_USER_FAILURE = 'REGISTER_USER_FAILURE';
8 | export const REGISTER_USER_REQUEST = 'REGISTER_USER_REQUEST';
9 |
10 | export const FETCH_PROTECTED_DATA_REQUEST = 'FETCH_PROTECTED_DATA_REQUEST';
11 | export const RECEIVE_PROTECTED_DATA = 'RECEIVE_PROTECTED_DATA';
12 |
--------------------------------------------------------------------------------
/migrations/versions/5aae5ada6624_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 5aae5ada6624
4 | Revises: None
5 | Create Date: 2016-02-27 14:15:21.751691
6 |
7 | """
8 |
9 | # revision identifiers, used by Alembic.
10 | revision = '5aae5ada6624'
11 | down_revision = None
12 |
13 | from alembic import op
14 | import sqlalchemy as sa
15 |
16 |
17 | def upgrade():
18 | ### commands auto generated by Alembic - please adjust! ###
19 | pass
20 | ### end Alembic commands ###
21 |
22 |
23 | def downgrade():
24 | ### commands auto generated by Alembic - please adjust! ###
25 | pass
26 | ### end Alembic commands ###
27 |
--------------------------------------------------------------------------------
/react-ui/src/reducers/data.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_PROTECTED_DATA, FETCH_PROTECTED_DATA_REQUEST } from '../constants';
2 | import { createReducer } from '../utils/misc';
3 |
4 | const initialState = {
5 | data: null,
6 | isFetching: false,
7 | loaded: false
8 | };
9 |
10 | export default createReducer(initialState, {
11 | [RECEIVE_PROTECTED_DATA]: (state, payload) =>
12 | Object.assign({}, state, {
13 | data: payload.data,
14 | isFetching: false,
15 | loaded: true
16 | }),
17 | [FETCH_PROTECTED_DATA_REQUEST]: state =>
18 | Object.assign({}, state, {
19 | isFetching: true
20 | })
21 | });
22 |
--------------------------------------------------------------------------------
/react-ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | cra-flask
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/migrations/versions/a7bca515a457_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: a7bca515a457
4 | Revises: ed657e16ce20
5 | Create Date: 2017-06-30 14:59:02.085857
6 |
7 | """
8 |
9 | # revision identifiers, used by Alembic.
10 | revision = 'a7bca515a457'
11 | down_revision = 'ed657e16ce20'
12 |
13 | from alembic import op
14 | import sqlalchemy as sa
15 |
16 |
17 | def upgrade():
18 | ### commands auto generated by Alembic - please adjust! ###
19 | pass
20 | ### end Alembic commands ###
21 |
22 |
23 | def downgrade():
24 | ### commands auto generated by Alembic - please adjust! ###
25 | pass
26 | ### end Alembic commands ###
27 |
--------------------------------------------------------------------------------
/migrations/versions/ed657e16ce20_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: ed657e16ce20
4 | Revises: 41c073a46b63
5 | Create Date: 2016-08-28 11:50:20.973452
6 |
7 | """
8 |
9 | # revision identifiers, used by Alembic.
10 | revision = 'ed657e16ce20'
11 | down_revision = '41c073a46b63'
12 |
13 | from alembic import op
14 | import sqlalchemy as sa
15 |
16 |
17 | def upgrade():
18 | ### commands auto generated by Alembic - please adjust! ###
19 | pass
20 | ### end Alembic commands ###
21 |
22 |
23 | def downgrade():
24 | ### commands auto generated by Alembic - please adjust! ###
25 | pass
26 | ### end Alembic commands ###
27 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alembic==0.8.7
2 | bcrypt==3.1.0
3 | cffi==1.7.0
4 | click==6.6
5 | coverage==4.2
6 | Flask==0.12.3
7 | Flask-Bcrypt==0.7.1
8 | Flask-Migrate==2.0.0
9 | Flask-Script==2.0.5
10 | Flask-SQLAlchemy==2.1
11 | Flask-Testing==0.5.0
12 | GitHub-Flask==3.1.3
13 | gunicorn==19.6.0
14 | itsdangerous==0.24
15 | Jinja2==2.10.1
16 | Mako==1.0.4
17 | MarkupSafe==0.23
18 | mysql-connector==2.1.4
19 | psycopg2==2.7.1
20 | py==1.4.31
21 | py-bcrypt==0.4
22 | pycparser==2.14
23 | pytest==3.0.1
24 | pytest-cov==2.3.1
25 | pytest-flask==0.10.0
26 | python-editor==1.0.1
27 | requests==2.20.0
28 | six==1.10.0
29 | SQLAlchemy==1.3.0
30 | Werkzeug==0.11.10
31 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from basedir import basedir
3 | import os
4 | import shutil
5 | import sys
6 |
7 |
8 | def main():
9 | argv = []
10 |
11 | argv.extend(sys.argv[1:])
12 |
13 | pytest.main(argv)
14 |
15 | try:
16 | os.remove(os.path.join(basedir, '.coverage'))
17 |
18 | except OSError:
19 | pass
20 |
21 | try:
22 | shutil.rmtree(os.path.join(basedir, '.cache'))
23 |
24 | except OSError:
25 | pass
26 |
27 | try:
28 | shutil.rmtree(os.path.join(basedir, 'tests/.cache'))
29 | except OSError:
30 | pass
31 |
32 |
33 |
34 | if __name__ == '__main__':
35 | main()
36 |
--------------------------------------------------------------------------------
/react-ui/src/components/Analytics.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import * as actionCreators from '../actions/auth';
5 |
6 | const Analytics = () => (
7 |
8 |
Analytics
9 |
10 |
11 | );
12 |
13 | function mapStateToProps(state) {
14 | return {
15 | isRegistering: state.auth.isRegistering,
16 | registerStatusText: state.auth.registerStatusText
17 | };
18 | }
19 |
20 | function mapDispatchToProps(dispatch) {
21 | return bindActionCreators(actionCreators, dispatch);
22 | }
23 |
24 | export default connect(
25 | mapStateToProps,
26 | mapDispatchToProps
27 | )(Analytics);
28 |
--------------------------------------------------------------------------------
/application/models.py:
--------------------------------------------------------------------------------
1 | from index import db, bcrypt
2 |
3 |
4 | class User(db.Model):
5 | id = db.Column(db.Integer(), primary_key=True)
6 | email = db.Column(db.String(255), unique=True)
7 | password = db.Column(db.String(255))
8 |
9 | def __init__(self, email, password):
10 | self.email = email
11 | self.active = True
12 | self.password = User.hashed_password(password)
13 |
14 | @staticmethod
15 | def hashed_password(password):
16 | return bcrypt.generate_password_hash(password)
17 |
18 | @staticmethod
19 | def get_user_with_email_and_password(email, password):
20 | user = User.query.filter_by(email=email).first()
21 | if user and bcrypt.check_password_hash(user.password, password):
22 | return user
23 | else:
24 | return None
25 |
--------------------------------------------------------------------------------
/react-ui/src/utils/misc.js:
--------------------------------------------------------------------------------
1 | /* eslint max-len: 0, no-param-reassign: 0 */
2 |
3 | export function createConstants(...constants) {
4 | return constants.reduce((acc, constant) => {
5 | acc[constant] = constant;
6 | return acc;
7 | }, {});
8 | }
9 |
10 | export function createReducer(initialState, reducerMap) {
11 | return (state = initialState, action) => {
12 | const reducer = reducerMap[action.type];
13 |
14 | return reducer ? reducer(state, action.payload) : state;
15 | };
16 | }
17 |
18 | export function parseJSON(response) {
19 | return response.data;
20 | }
21 |
22 | export function validateEmail(email) {
23 | const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
24 | return re.test(email);
25 | }
26 |
--------------------------------------------------------------------------------
/react-ui/src/components/NotFound.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { bindActionCreators } from 'redux';
4 | import * as actionCreators from '../actions/auth';
5 |
6 | class NotFound extends Component {
7 | // eslint-disable-line react/prefer-stateless-function
8 | render() {
9 | return (
10 |
11 |
Not Found
12 |
13 | );
14 | }
15 | }
16 |
17 | function mapStateToProps(state) {
18 | return {
19 | token: state.auth.token,
20 | userName: state.auth.userName,
21 | isAuthenticated: state.auth.isAuthenticated
22 | };
23 | }
24 |
25 | function mapDispatchToProps(dispatch) {
26 | return bindActionCreators(actionCreators, dispatch);
27 | }
28 |
29 | export default connect(
30 | mapStateToProps,
31 | mapDispatchToProps
32 | )(NotFound);
33 |
--------------------------------------------------------------------------------
/testing_config.py:
--------------------------------------------------------------------------------
1 | from flask_testing import TestCase
2 | from application.app import app, db
3 | from application.models import User
4 | import os
5 | from setup import basedir
6 | import json
7 |
8 |
9 | class BaseTestConfig(TestCase):
10 | default_user = {
11 | "email": "default@gmail.com",
12 | "password": "something2"
13 | }
14 |
15 | def create_app(self):
16 | app.config.from_object('config.TestingConfig')
17 | return app
18 |
19 | def setUp(self):
20 | self.app = self.create_app().test_client()
21 | db.create_all()
22 | res = self.app.post(
23 | "/api/create_user",
24 | data=json.dumps(self.default_user),
25 | content_type='application/json'
26 | )
27 |
28 | self.token = json.loads(res.data.decode("utf-8"))["token"]
29 |
30 | def tearDown(self):
31 | db.session.remove()
32 | db.drop_all()
33 |
--------------------------------------------------------------------------------
/migrations/versions/41c073a46b63_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 41c073a46b63
4 | Revises: 5aae5ada6624
5 | Create Date: 2016-04-08 00:34:28.952489
6 |
7 | """
8 |
9 | # revision identifiers, used by Alembic.
10 | revision = '41c073a46b63'
11 | down_revision = '5aae5ada6624'
12 |
13 | from alembic import op
14 | import sqlalchemy as sa
15 |
16 |
17 | def upgrade():
18 | ### commands auto generated by Alembic - please adjust! ###
19 | op.create_table('user',
20 | sa.Column('id', sa.Integer(), nullable=False),
21 | sa.Column('email', sa.String(length=255), nullable=True),
22 | sa.Column('password', sa.String(length=255), nullable=True),
23 | sa.PrimaryKeyConstraint('id'),
24 | sa.UniqueConstraint('email')
25 | )
26 | ### end Alembic commands ###
27 |
28 |
29 | def downgrade():
30 | ### commands auto generated by Alembic - please adjust! ###
31 | op.drop_table('user')
32 | ### end Alembic commands ###
33 |
--------------------------------------------------------------------------------
/react-ui/src/actions/data.js:
--------------------------------------------------------------------------------
1 | import { FETCH_PROTECTED_DATA_REQUEST, RECEIVE_PROTECTED_DATA } from '../constants/index';
2 | import { parseJSON } from '../utils/misc';
3 | import { dataAboutUser } from '../utils/http_functions';
4 | import { logoutAndRedirect } from './auth';
5 |
6 | export function receiveProtectedData(data) {
7 | return {
8 | type: RECEIVE_PROTECTED_DATA,
9 | payload: {
10 | data
11 | }
12 | };
13 | }
14 |
15 | export function fetchProtectedDataRequest() {
16 | return {
17 | type: FETCH_PROTECTED_DATA_REQUEST
18 | };
19 | }
20 |
21 | export const fetchProtectedData = token => dispatch => {
22 | dispatch(fetchProtectedDataRequest());
23 | dataAboutUser(token)
24 | .then(parseJSON)
25 | .then(response => {
26 | dispatch(receiveProtectedData(response.result));
27 | })
28 | .catch(error => {
29 | if (error.status === 401) {
30 | dispatch(logoutAndRedirect(error));
31 | }
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/react-ui/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import invariant from 'redux-immutable-state-invariant';
3 | import thunk from 'redux-thunk';
4 | import { routerMiddleware } from 'react-router-redux';
5 | import createHistory from 'history/createBrowserHistory';
6 | import rootReducer from '../reducers';
7 |
8 | const resolveMiddleware = () => {
9 | const history = createHistory();
10 | const historyMiddleware = routerMiddleware(history);
11 |
12 | const middleware = [thunk, historyMiddleware];
13 | if (process.env.NODE_ENV !== 'production') {
14 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
15 | return composeEnhancer(applyMiddleware(...middleware, invariant({ ignore: ['queries'] })));
16 | }
17 | return compose(applyMiddleware(...middleware));
18 | };
19 |
20 | const middleware = resolveMiddleware();
21 | const store = createStore(rootReducer, middleware);
22 |
23 | export default store;
24 |
--------------------------------------------------------------------------------
/react-ui/src/utils/http_functions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const tokenConfig = token => ({
4 | headers: {
5 | Authorization: token // eslint-disable-line quote-props
6 | }
7 | });
8 |
9 | export function validateToken(token) {
10 | return axios.post('/api/is_token_valid', {
11 | token
12 | });
13 | }
14 |
15 | export function getGithubAccess() {
16 | window.open(
17 | '/github-login',
18 | '_blank' // <- This is what makes it open in a new window.
19 | );
20 | }
21 |
22 | export function createUser(email, password) {
23 | return axios.post('api/create_user', {
24 | email,
25 | password
26 | });
27 | }
28 |
29 | export function getToken(email, password) {
30 | return axios.post('api/getToken', {
31 | email,
32 | password
33 | });
34 | }
35 |
36 | export function hasGithubAccess(token) {
37 | return axios.get('api/hasGithubAccess', tokenConfig(token));
38 | }
39 |
40 | export function dataAboutUser(token) {
41 | return axios.get('api/user', tokenConfig(token));
42 | }
43 |
--------------------------------------------------------------------------------
/react-ui/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Provider, connect } from 'react-redux';
3 | import { Router, withRouter } from 'react-router-dom';
4 |
5 | import './App.css';
6 | import createHistory from 'history/createBrowserHistory';
7 | import { AuthRoutes, NonAuthRoutes } from './routes';
8 | import Main from './containers/App';
9 |
10 | import store from './store/configureStore';
11 |
12 | const history = createHistory();
13 |
14 | const App = ({ isAuthenticated }) => {
15 | return {isAuthenticated ? : };
16 | };
17 |
18 | function mapStateToProps(state) {
19 | return { isAuthenticated: state.auth.isAuthenticated };
20 | }
21 |
22 | const ConnectedApp = withRouter(connect(mapStateToProps)(App));
23 |
24 | class AppContainer extends Component {
25 | render() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | export default AppContainer;
37 |
--------------------------------------------------------------------------------
/react-ui/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Switch } from 'react-router-dom';
3 |
4 | import { HomeContainer } from './containers/HomeContainer';
5 | import LoginView from './components/LoginView';
6 | import RegisterView from './components/RegisterView';
7 | import ProtectedView from './components/ProtectedView';
8 | import Analytics from './components/Analytics';
9 | import NotFound from './components/NotFound';
10 |
11 | export const AuthRoutes = () => (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export const NonAuthRoutes = () => (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Daniel Ternyak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/application/utils/auth.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from flask import request, g, jsonify
3 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
4 | from itsdangerous import SignatureExpired, BadSignature
5 | from index import app
6 |
7 | TWO_WEEKS = 1209600
8 |
9 |
10 | def generate_token(user, expiration=TWO_WEEKS):
11 | s = Serializer(app.config['SECRET_KEY'], expires_in=expiration)
12 | token = s.dumps({
13 | 'id': user.id,
14 | 'email': user.email,
15 | }).decode('utf-8')
16 | return token
17 |
18 |
19 | def verify_token(token):
20 | s = Serializer(app.config['SECRET_KEY'])
21 | try:
22 | data = s.loads(token)
23 | except (BadSignature, SignatureExpired):
24 | return None
25 | return data
26 |
27 |
28 | def requires_auth(f):
29 | @wraps(f)
30 | def decorated(*args, **kwargs):
31 | token = request.headers.get('Authorization', None)
32 | if token:
33 | string_token = token.encode('ascii', 'ignore')
34 | user = verify_token(string_token)
35 | if user:
36 | g.current_user = user
37 | return f(*args, **kwargs)
38 |
39 | return jsonify(message="Authentication is required to access this resource"), 401
40 |
41 | return decorated
42 |
--------------------------------------------------------------------------------
/react-ui/src/components/ProtectedView.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { bindActionCreators } from 'redux';
5 | import * as actionCreators from '../actions/data';
6 |
7 | class ProtectedView extends Component {
8 | componentDidMount() {
9 | this.fetchData();
10 | }
11 |
12 | fetchData() {
13 | const token = this.props.token;
14 | this.props.fetchProtectedData(token);
15 | }
16 |
17 | render() {
18 | return (
19 |
20 | {!this.props.loaded ? (
21 |
Loading data...
22 | ) : (
23 |
24 |
Welcome back, {this.props.userName}!
25 | {this.props.data.data.email}
26 |
27 | )}
28 |
29 | );
30 | }
31 | }
32 |
33 | ProtectedView.propTypes = {
34 | fetchProtectedData: PropTypes.func,
35 | loaded: PropTypes.bool,
36 | userName: PropTypes.string,
37 | data: PropTypes.any,
38 | token: PropTypes.string
39 | };
40 |
41 | function mapStateToProps(state) {
42 | return { data: state.data, token: state.auth.token, loaded: state.data.loaded, isFetching: state.data.isFetching };
43 | }
44 |
45 | function mapDispatchToProps(dispatch) {
46 | return bindActionCreators(actionCreators, dispatch);
47 | }
48 |
49 | export default connect(
50 | mapStateToProps,
51 | mapDispatchToProps
52 | )(ProtectedView);
53 |
--------------------------------------------------------------------------------
/react-ui/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "plugins": ["react", "prettier"],
4 | "extends": ["eslint:recommended", "plugin:react/recommended", "airbnb", "prettier", "prettier/react"],
5 | "parserOptions": {
6 | "ecmaVersion": 8,
7 | "ecmaFeatures": {
8 | "experimentalObjectRestSpread": true,
9 | "jsx": true
10 | },
11 | "sourceType": "module"
12 | },
13 |
14 | "env": {
15 | "browser": true
16 | },
17 | "rules": {
18 | "jsx-a11y/click-events-have-key-events": "off",
19 | "jsx-a11y/no-static-element-interactions": "off",
20 | "jsx-a11y/anchor-is-valid": "off",
21 | "jsx-a11y/mouse-events-have-key-events": "off",
22 | "react/prop-types": "off",
23 | "react/no-array-index-key": "off",
24 | "react/jsx-filename-extension": "off",
25 | "react/forbid-prop-types": "off",
26 | "react/destructuring-assignment": "warn",
27 | "prettier/prettier": ["error", { "singleQuote": true, "semi": true, "printWidth": 120 }],
28 | "class-methods-use-this": "off",
29 | "import/extensions": "off",
30 | "no-underscore-dangle": "off",
31 | "no-shadow": "off",
32 | "no-nested-ternary": "off",
33 | "no-restricted-syntax": "off",
34 | "prefer-destructuring": "warn"
35 | },
36 | "globals": {
37 | "describe": true,
38 | "before": true,
39 | "beforeEach": true,
40 | "after": true,
41 | "afterEach": true,
42 | "it": true,
43 | "xit": true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/react-ui/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/application/app.py:
--------------------------------------------------------------------------------
1 | from flask import request, render_template, jsonify, url_for, redirect, g
2 | from .models import User
3 | from index import app, db
4 | from sqlalchemy.exc import IntegrityError
5 | from .utils.auth import generate_token, requires_auth, verify_token
6 |
7 |
8 | @app.route('/', methods=['GET'])
9 | def index():
10 | return render_template('index.html')
11 |
12 |
13 | @app.route('/', methods=['GET'])
14 | def any_root_path(path):
15 | return render_template('index.html')
16 |
17 |
18 | @app.route("/api/user", methods=["GET"])
19 | @requires_auth
20 | def get_user():
21 | return jsonify(result=g.current_user)
22 |
23 |
24 | @app.route("/api/create_user", methods=["POST"])
25 | def create_user():
26 | incoming = request.get_json()
27 | user = User(
28 | email=incoming["email"],
29 | password=incoming["password"]
30 | )
31 | db.session.add(user)
32 |
33 | try:
34 | db.session.commit()
35 | except IntegrityError:
36 | return jsonify(message="User with that email already exists"), 409
37 |
38 | new_user = User.query.filter_by(email=incoming["email"]).first()
39 |
40 | return jsonify(
41 | id=user.id,
42 | token=generate_token(new_user)
43 | )
44 |
45 |
46 | @app.route("/api/getToken", methods=["POST"])
47 | def getToken():
48 | incoming = request.get_json()
49 | user = User.get_user_with_email_and_password(incoming["email"], incoming["password"])
50 | if user:
51 | return jsonify(token=generate_token(user))
52 |
53 | return jsonify(error=True), 403
54 |
55 |
56 | @app.route("/api/is_token_valid", methods=["POST"])
57 | def is_token_valid():
58 | incoming = request.get_json()
59 | is_valid = verify_token(incoming["token"])
60 |
61 | if is_valid:
62 | return jsonify(token_is_valid=True)
63 | else:
64 | return jsonify(token_is_valid=False), 403
65 |
--------------------------------------------------------------------------------
/react-ui/src/components/DetermineAuth.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { bindActionCreators } from 'redux';
5 | import * as actionCreators from '../actions/auth';
6 |
7 | function mapStateToProps(state) {
8 | return {
9 | token: state.auth.token,
10 | userName: state.auth.userName,
11 | isAuthenticated: state.auth.isAuthenticated
12 | };
13 | }
14 |
15 | function mapDispatchToProps(dispatch) {
16 | return bindActionCreators(actionCreators, dispatch);
17 | }
18 |
19 | export default function DetermineAuth(Component) {
20 | class AuthenticatedComponent extends Component {
21 | componentWillMount() {
22 | this.checkAuth();
23 | this.state = {
24 | loaded_if_needed: false
25 | };
26 | }
27 |
28 | componentWillReceiveProps(nextProps) {
29 | this.checkAuth(nextProps);
30 | }
31 |
32 | checkAuth(props = this.props) {
33 | if (!props.isAuthenticated) {
34 | const token = localStorage.getItem('token');
35 | if (token) {
36 | fetch('api/is_token_valid', {
37 | method: 'post',
38 | credentials: 'include',
39 | headers: {
40 | Accept: 'application/json',
41 | 'Content-Type': 'application/json'
42 | },
43 | body: JSON.stringify({ token })
44 | }).then(res => {
45 | if (res.status === 200) {
46 | this.props.loginUserSuccess(token);
47 | this.setState({
48 | loaded_if_needed: true
49 | });
50 | }
51 | });
52 | }
53 | } else {
54 | this.setState({
55 | loaded_if_needed: true
56 | });
57 | }
58 | }
59 |
60 | render() {
61 | return {this.state.loaded_if_needed ? : null}
;
62 | }
63 | }
64 |
65 | AuthenticatedComponent.propTypes = {
66 | loginUserSuccess: PropTypes.func.isRequired
67 | };
68 |
69 | return connect(
70 | mapStateToProps,
71 | mapDispatchToProps
72 | )(AuthenticatedComponent);
73 | }
74 |
--------------------------------------------------------------------------------
/react-ui/src/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 | import { bindActionCreators } from 'redux';
5 |
6 | import getMuiTheme from 'material-ui/styles/getMuiTheme';
7 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
8 | import * as actionCreators from '../../actions/auth';
9 |
10 | /* application components */
11 | import Header from '../../components/Header';
12 | import Footer from '../../components/Footer';
13 |
14 | import { checkAuth } from '../../utils/checkAuth';
15 | import './styles/app.scss'; // eslint-disable-line no-unused-vars
16 |
17 | class Main extends Component {
18 | constructor(props) {
19 | super(props);
20 | this.state = {
21 | loaded: false
22 | };
23 | }
24 |
25 | componentWillMount() {
26 | const result = checkAuth().then(response => {
27 | if (response.status === 200) {
28 | this.props.loginUserSuccess(localStorage.getItem('token'));
29 | this.setState({ loaded: true });
30 | }
31 | });
32 |
33 | if (result) {
34 | this.props.loginUserSuccess(localStorage.getItem('token'));
35 | this.setState({ loaded: true });
36 | }
37 | }
38 |
39 | render() {
40 | return (
41 |
42 |
43 |
44 |
51 | {this.props.children}
52 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | Main.propTypes = {
63 | children: PropTypes.any
64 | };
65 |
66 | function mapStateToProps(state) {
67 | return {
68 | token: state.auth.token,
69 | userName: state.auth.userName,
70 | isAuthenticated: state.auth.isAuthenticated
71 | };
72 | }
73 |
74 | function mapDispatchToProps(dispatch) {
75 | return bindActionCreators(actionCreators, dispatch);
76 | }
77 |
78 | export default connect(
79 | mapStateToProps,
80 | mapDispatchToProps
81 | )(Main);
82 |
--------------------------------------------------------------------------------
/react-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cra-flask",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "autoprefixer": "^9.4.10",
7 | "axios": "^0.18.0",
8 | "bootstrap": "^4.3.1",
9 | "bootstrap-sass": "^3.4.1",
10 | "classnames": "^2.2.6",
11 | "csswring": "^7.0.0",
12 | "deep-equal": "^1.0.1",
13 | "expect": "^24.1.0",
14 | "express": "^4.16.4",
15 | "express-open-in-editor": "^3.1.1",
16 | "gapi": "0.0.3",
17 | "history": "^4.7.2",
18 | "http-proxy": "^1.17.0",
19 | "jquery": "^3.3.1",
20 | "jwt-decode": "^2.2.0",
21 | "less": "^3.9.0",
22 | "lodash": "^4.17.11",
23 | "material-ui": "^0.20.2",
24 | "morgan": "^1.9.1",
25 | "node-sass": "^4.11.0",
26 | "prop-types": "^15.7.2",
27 | "q": "^1.5.1",
28 | "qs": "^6.6.0",
29 | "rc-datepicker": "^5.0.13",
30 | "react": "^16.8.4",
31 | "react-addons-css-transition-group": "^15.6.2",
32 | "react-calendar-component": "^3.0.0",
33 | "react-date-picker": "^7.3.0",
34 | "react-datepicker": "^2.1.0",
35 | "react-document-meta": "^3.0.0-beta.2",
36 | "react-dom": "^16.8.4",
37 | "react-forms": "^1.0.0-rc6",
38 | "react-loading-order-with-animation": "^1.0.0",
39 | "react-onclickoutside": "^6.8.0",
40 | "react-redux": "^6.0.1",
41 | "react-router": "^4.3.1",
42 | "react-router-dom": "^4.3.1",
43 | "react-router-redux": "^4.0.8",
44 | "react-tap-event-plugin": "^3.0.3",
45 | "react-transform-hmr": "^1.0.4",
46 | "redux": "^4.0.1",
47 | "redux-form": "^8.1.0",
48 | "redux-immutable-state-invariant": "^2.1.0",
49 | "redux-logger": "^3.0.6",
50 | "redux-thunk": "^2.3.0",
51 | "rimraf": "^2.6.3",
52 | "yargs": "^13.2.2"
53 | },
54 | "devDependencies": {
55 | "eslint-config-airbnb": "^17.1.0",
56 | "eslint-config-prettier": "^4.1.0",
57 | "eslint-plugin-import": "^2.16.0",
58 | "eslint-plugin-prettier": "^3.0.1",
59 | "eslint-plugin-react": "^7.12.4",
60 | "prettier": "^1.16.4",
61 | "react-scripts": "2.1.8"
62 | },
63 | "scripts": {
64 | "start": "react-scripts start",
65 | "build": "react-scripts build",
66 | "test": "react-scripts test --env=jsdom",
67 | "eject": "react-scripts eject"
68 | },
69 | "proxy": "http://localhost:5000",
70 | "browserslist": [
71 | ">0.2%",
72 | "not dead",
73 | "not ie <= 11",
74 | "not op_mini all"
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from alembic import context
3 | from sqlalchemy import engine_from_config, pool
4 | from logging.config import fileConfig
5 |
6 | # this is the Alembic Config object, which provides
7 | # access to the values within the .ini file in use.
8 | config = context.config
9 |
10 | # Interpret the config file for Python logging.
11 | # This line sets up loggers basically.
12 | fileConfig(config.config_file_name)
13 |
14 | # add your model's MetaData object here
15 | # for 'autogenerate' support
16 | # from myapp import mymodel
17 | # target_metadata = mymodel.Base.metadata
18 | from flask import current_app
19 | config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
20 | target_metadata = current_app.extensions['migrate'].db.metadata
21 |
22 | # other values from the config, defined by the needs of env.py,
23 | # can be acquired:
24 | # my_important_option = config.get_main_option("my_important_option")
25 | # ... etc.
26 |
27 | def run_migrations_offline():
28 | """Run migrations in 'offline' mode.
29 |
30 | This configures the context with just a URL
31 | and not an Engine, though an Engine is acceptable
32 | here as well. By skipping the Engine creation
33 | we don't even need a DBAPI to be available.
34 |
35 | Calls to context.execute() here emit the given string to the
36 | script output.
37 |
38 | """
39 | url = config.get_main_option("sqlalchemy.url")
40 | context.configure(url=url)
41 |
42 | with context.begin_transaction():
43 | context.run_migrations()
44 |
45 | def run_migrations_online():
46 | """Run migrations in 'online' mode.
47 |
48 | In this scenario we need to create an Engine
49 | and associate a connection with the context.
50 |
51 | """
52 | engine = engine_from_config(
53 | config.get_section(config.config_ini_section),
54 | prefix='sqlalchemy.',
55 | poolclass=pool.NullPool)
56 |
57 | connection = engine.connect()
58 | context.configure(
59 | connection=connection,
60 | target_metadata=target_metadata
61 | )
62 |
63 | try:
64 | with context.begin_transaction():
65 | context.run_migrations()
66 | finally:
67 | connection.close()
68 |
69 | if context.is_offline_mode():
70 | run_migrations_offline()
71 | else:
72 | run_migrations_online()
73 |
74 |
--------------------------------------------------------------------------------
/react-ui/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import jwtDecode from 'jwt-decode';
2 | import { isNull } from 'lodash';
3 |
4 | import { createReducer } from '../utils/misc';
5 | import {
6 | LOGIN_USER_SUCCESS,
7 | LOGIN_USER_FAILURE,
8 | LOGIN_USER_REQUEST,
9 | LOGOUT_USER,
10 | REGISTER_USER_FAILURE,
11 | REGISTER_USER_REQUEST,
12 | REGISTER_USER_SUCCESS
13 | } from '../constants/index';
14 |
15 | const initialState = {
16 | token: null,
17 | userName: null,
18 | isAuthenticated: false,
19 | isAuthenticating: false,
20 | statusText: null,
21 | isRegistering: false,
22 | isRegistered: false,
23 | registerStatusText: null
24 | };
25 |
26 | const decodeTokenEmail = token => {
27 | if (isNull(token)) return null;
28 | try {
29 | return jwtDecode('asdfasdf').email;
30 | } catch (err) {
31 | return null;
32 | }
33 | };
34 |
35 | export default createReducer(initialState, {
36 | [LOGIN_USER_REQUEST]: state =>
37 | Object.assign({}, state, {
38 | isAuthenticating: true,
39 | statusText: null
40 | }),
41 | [LOGIN_USER_SUCCESS]: (state, { token }) =>
42 | Object.assign({}, state, {
43 | isAuthenticating: false,
44 | isAuthenticated: true,
45 | token,
46 | userName: decodeTokenEmail(token),
47 | statusText: 'You have been successfully logged in.'
48 | }),
49 | [LOGIN_USER_FAILURE]: (state, payload) =>
50 | Object.assign({}, state, {
51 | isAuthenticating: false,
52 | isAuthenticated: false,
53 | token: null,
54 | userName: null,
55 | statusText: `Authentication Error: ${payload.status} ${payload.statusText}`
56 | }),
57 | [LOGOUT_USER]: state =>
58 | Object.assign({}, state, {
59 | isAuthenticated: false,
60 | token: null,
61 | userName: null,
62 | statusText: 'You have been successfully logged out.'
63 | }),
64 | [REGISTER_USER_SUCCESS]: (state, { token }) =>
65 | Object.assign({}, state, {
66 | isAuthenticating: false,
67 | isAuthenticated: true,
68 | isRegistering: false,
69 | token,
70 | userName: decodeTokenEmail(token),
71 | registerStatusText: 'You have been successfully logged in.'
72 | }),
73 | [REGISTER_USER_REQUEST]: state =>
74 | Object.assign({}, state, {
75 | isRegistering: true
76 | }),
77 | [REGISTER_USER_FAILURE]: (state, payload) =>
78 | Object.assign({}, state, {
79 | isAuthenticated: false,
80 | token: null,
81 | userName: null,
82 | registerStatusText: `Register Error: ${payload.status} ${payload.statusText}`
83 | })
84 | });
85 |
--------------------------------------------------------------------------------
/react-ui/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withRouter } from 'react-router';
4 | import { connect } from 'react-redux';
5 | import { bindActionCreators } from 'redux';
6 | import AppBar from 'material-ui/AppBar';
7 | import LeftNav from 'material-ui/Drawer';
8 | import MenuItem from 'material-ui/MenuItem';
9 | import FlatButton from 'material-ui/FlatButton';
10 | import Divider from 'material-ui/Divider';
11 |
12 | import * as actionCreators from '../../actions/auth';
13 |
14 | class Header extends Component {
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | open: false
19 | };
20 | }
21 |
22 | dispatchNewRoute(route) {
23 | this.props.history.push(route);
24 | this.setState({ open: false });
25 | }
26 |
27 | handleClickOutside() {
28 | this.setState({ open: false });
29 | }
30 |
31 | logout(e) {
32 | e.preventDefault();
33 | this.props.logoutAndRedirect(this.props.history);
34 | this.setState({ open: false });
35 | }
36 |
37 | openNav() {
38 | this.setState({ open: true });
39 | }
40 |
41 | render() {
42 | return (
43 |
65 | );
66 | }
67 | }
68 |
69 | Header.propTypes = {
70 | logoutAndRedirect: PropTypes.func,
71 | isAuthenticated: PropTypes.bool
72 | };
73 |
74 | function mapStateToProps(state) {
75 | return { token: state.auth.token, userName: state.auth.userName, isAuthenticated: state.auth.isAuthenticated };
76 | }
77 |
78 | function mapDispatchToProps(dispatch) {
79 | return bindActionCreators(actionCreators, dispatch);
80 | }
81 |
82 | export default withRouter(
83 | connect(
84 | mapStateToProps,
85 | mapDispatchToProps
86 | )(Header)
87 | );
88 |
--------------------------------------------------------------------------------
/react-ui/bootstrap.rc:
--------------------------------------------------------------------------------
1 | ---
2 | # Output debugging info
3 | # loglevel: debug
4 |
5 | # Major version of Bootstrap: 3 or 4
6 | bootstrapVersion: 3
7 |
8 | # Webpack loaders, order matters
9 | styleLoaders:
10 | - style
11 | - css
12 | - sass
13 |
14 | # Extract styles to stand-alone css file
15 | # Different settings for different environments can be used,
16 | # It depends on value of NODE_ENV environment variable
17 | # This param can also be set in webpack config:
18 | # entry: 'bootstrap-loader/extractStyles'
19 | # extractStyles: false
20 | # env:
21 | # development:
22 | # extractStyles: false
23 | # production:
24 | # extractStyles: true
25 |
26 |
27 | # Customize Bootstrap variables that get imported before the original Bootstrap variables.
28 | # Thus, derived Bootstrap variables can depend on values from here.
29 | # See the Bootstrap _variables.scss file for examples of derived Bootstrap variables.
30 | #
31 | # preBootstrapCustomizations: ./path/to/bootstrap/pre-customizations.scss
32 |
33 |
34 | # This gets loaded after bootstrap/variables is loaded
35 | # Thus, you may customize Bootstrap variables
36 | # based on the values established in the Bootstrap _variables.scss file
37 | #
38 | # bootstrapCustomizations: ./path/to/bootstrap/customizations.scss
39 |
40 |
41 | # Import your custom styles here
42 | # Usually this endpoint-file contains list of @imports of your application styles
43 | #
44 | # appStyles: ./path/to/your/app/styles/endpoint.scss
45 |
46 |
47 | ### Bootstrap styles
48 | styles:
49 |
50 | # Mixins
51 | mixins: true
52 |
53 | # Reset and dependencies
54 | normalize: true
55 | print: true
56 | glyphicons: true
57 |
58 | # Core CSS
59 | scaffolding: true
60 | type: true
61 | code: true
62 | grid: true
63 | tables: true
64 | forms: true
65 | buttons: true
66 |
67 | # Components
68 | component-animations: true
69 | dropdowns: true
70 | button-groups: true
71 | input-groups: true
72 | navs: true
73 | navbar: true
74 | breadcrumbs: true
75 | pagination: true
76 | pager: true
77 | labels: true
78 | badges: true
79 | jumbotron: true
80 | thumbnails: true
81 | alerts: true
82 | progress-bars: true
83 | media: true
84 | list-group: true
85 | panels: true
86 | wells: true
87 | responsive-embed: true
88 | close: true
89 |
90 | # Components w/ JavaScript
91 | modals: true
92 | tooltip: true
93 | popovers: true
94 | carousel: true
95 |
96 | # Utility classes
97 | utilities: true
98 | responsive-utilities: true
99 |
100 | ### Bootstrap scripts
101 | scripts:
102 | transition: true
103 | alert: true
104 | button: true
105 | carousel: true
106 | collapse: true
107 | dropdown: true
108 | modal: true
109 | tooltip: true
110 | popover: true
111 | scrollspy: true
112 | tab: true
113 | affix: true
114 | Status API Training Shop Blog About Pricing
115 |
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
1 | from testing_config import BaseTestConfig
2 | from application.models import User
3 | import json
4 | from application.utils import auth
5 |
6 |
7 | class TestAPI(BaseTestConfig):
8 | some_user = {
9 | "email": "one@gmail.com",
10 | "password": "something1"
11 | }
12 |
13 | def test_get_spa_from_index(self):
14 | result = self.app.get("/")
15 | self.assertIn('', result.data.decode("utf-8"))
16 |
17 | def test_create_new_user(self):
18 | self.assertIsNone(User.query.filter_by(
19 | email=self.some_user["email"]
20 | ).first())
21 |
22 | res = self.app.post(
23 | "/api/create_user",
24 | data=json.dumps(self.some_user),
25 | content_type='application/json'
26 | )
27 | self.assertEqual(res.status_code, 200)
28 | self.assertTrue(json.loads(res.data.decode("utf-8"))["token"])
29 | self.assertEqual(User.query.filter_by(email=self.some_user["email"]).first().email, self.some_user["email"])
30 |
31 | res2 = self.app.post(
32 | "/api/create_user",
33 | data=json.dumps(self.some_user),
34 | content_type='application/json'
35 | )
36 |
37 | self.assertEqual(res2.status_code, 409)
38 |
39 | def test_getToken_and_verify_token(self):
40 | res = self.app.post(
41 | "/api/getToken",
42 | data=json.dumps(self.default_user),
43 | content_type='application/json'
44 | )
45 |
46 | token = json.loads(res.data.decode("utf-8"))["token"]
47 | self.assertTrue(auth.verify_token(token))
48 | self.assertEqual(res.status_code, 200)
49 |
50 | res2 = self.app.post(
51 | "/api/is_token_valid",
52 | data=json.dumps({"token": token}),
53 | content_type='application/json'
54 | )
55 |
56 | self.assertTrue(json.loads(res2.data.decode("utf-8")), ["token_is_valid"])
57 |
58 | res3 = self.app.post(
59 | "/api/is_token_valid",
60 | data=json.dumps({"token": token + "something-else"}),
61 | content_type='application/json'
62 | )
63 |
64 | self.assertEqual(res3.status_code, 403)
65 |
66 | res4 = self.app.post(
67 | "/api/getToken",
68 | data=json.dumps(self.some_user),
69 | content_type='application/json'
70 | )
71 |
72 | self.assertEqual(res4.status_code, 403)
73 |
74 | def test_protected_route(self):
75 | headers = {
76 | 'Authorization': self.token,
77 | }
78 |
79 | bad_headers = {
80 | 'Authorization': self.token + "bad",
81 | }
82 |
83 | response = self.app.get('/api/user', headers=headers)
84 | self.assertEqual(response.status_code, 200)
85 | response2 = self.app.get('/api/user')
86 | self.assertEqual(response2.status_code, 401)
87 | response3 = self.app.get('/api/user', headers=bad_headers)
88 | self.assertEqual(response3.status_code, 401)
89 |
--------------------------------------------------------------------------------
/react-ui/src/actions/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOGIN_USER_SUCCESS,
3 | LOGIN_USER_FAILURE,
4 | LOGIN_USER_REQUEST,
5 | LOGOUT_USER,
6 | REGISTER_USER_FAILURE,
7 | REGISTER_USER_REQUEST,
8 | REGISTER_USER_SUCCESS
9 | } from '../constants/index';
10 |
11 | import { parseJSON } from '../utils/misc';
12 | import { getToken, createUser } from '../utils/http_functions';
13 |
14 | export function loginUserSuccess(token) {
15 | localStorage.setItem('token', token);
16 | return {
17 | type: LOGIN_USER_SUCCESS,
18 | payload: {
19 | token
20 | }
21 | };
22 | }
23 |
24 | export function loginUserFailure(error) {
25 | localStorage.removeItem('token');
26 | return {
27 | type: LOGIN_USER_FAILURE,
28 | payload: {
29 | status: error.response.status,
30 | statusText: error.response.statusText
31 | }
32 | };
33 | }
34 |
35 | export function loginUserRequest() {
36 | return { type: LOGIN_USER_REQUEST };
37 | }
38 |
39 | export function logout() {
40 | localStorage.removeItem('token');
41 | return { type: LOGOUT_USER };
42 | }
43 |
44 | export function logoutAndRedirect(history) {
45 | return dispatch => {
46 | dispatch(logout());
47 | history.push('/');
48 | };
49 | }
50 |
51 | export const redirectToRoute = route => {
52 | return () => {
53 | this.props.history.push(route);
54 | };
55 | };
56 |
57 | export const loginUser = (email, password, history) => dispatch => {
58 | dispatch(loginUserRequest());
59 | return getToken(email, password)
60 | .then(parseJSON)
61 | .then(response => {
62 | try {
63 | dispatch(loginUserSuccess(response.token));
64 | history.push('/main');
65 | } catch (error) {
66 | dispatch(
67 | loginUserFailure({
68 | response: {
69 | status: 403,
70 | statusText: 'Invalid token',
71 | error
72 | }
73 | })
74 | );
75 | }
76 | })
77 | .catch(error => {
78 | dispatch(
79 | loginUserFailure({
80 | response: {
81 | status: 403,
82 | statusText: 'Invalid username or password',
83 | error
84 | }
85 | })
86 | );
87 | });
88 | };
89 |
90 | export function registerUserRequest() {
91 | return { type: REGISTER_USER_REQUEST };
92 | }
93 |
94 | export function registerUserSuccess(token) {
95 | localStorage.setItem('token', token);
96 | return {
97 | type: REGISTER_USER_SUCCESS,
98 | payload: {
99 | token
100 | }
101 | };
102 | }
103 |
104 | export function registerUserFailure(error) {
105 | localStorage.removeItem('token');
106 | return {
107 | type: REGISTER_USER_FAILURE,
108 | payload: {
109 | status: error.response.status,
110 | statusText: error.response.statusText
111 | }
112 | };
113 | }
114 |
115 | export const registerUser = (email, password, history) => dispatch => {
116 | dispatch(registerUserRequest());
117 | return createUser(email, password)
118 | .then(parseJSON)
119 | .then(response => {
120 | try {
121 | dispatch(registerUserSuccess(response.token));
122 | history.push('/main');
123 | } catch (e) {
124 | dispatch(
125 | registerUserFailure({
126 | response: {
127 | status: 403,
128 | statusText: 'Invalid token'
129 | }
130 | })
131 | );
132 | }
133 | })
134 | .catch(error => {
135 | dispatch(
136 | registerUserFailure({
137 | response: {
138 | status: 403,
139 | statusText: 'User with that email already exists',
140 | error
141 | }
142 | })
143 | );
144 | });
145 | };
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cra-flask #
2 |
3 | Boilerplate application for a Flask JWT Backend and an unejected React/Redux Front-End with Material UI.
4 |
5 | A Create-React-App conversion of [React-Redux-Flask](https://github.com/dternyak/React-Redux-Flask)
6 |
7 | * Python 2.7+ or 3.x
8 | * Pytest
9 | * Heroku
10 | * Flask
11 | * React
12 | * Redux
13 | * React-Router v4
14 | * React-Router-Redux
15 | * SCSS processing
16 | * Create-React-App
17 |
18 | 
19 |
20 | ### Create DB
21 | ```sh
22 | $ export DATABASE_URL="postgresql://username:password@localhost/mydatabase"
23 |
24 | or
25 |
26 | $ export DATABASE_URL="mysql+mysqlconnector://username:password@localhost/mydatabase"
27 |
28 | or
29 |
30 | $ export DATABASE_URL="sqlite:///your.db"
31 |
32 | More about connection strings in this [flask config guide](http://flask-sqlalchemy.pocoo.org/2.1/config/)
33 |
34 | $ python manage.py create_db
35 | $ python manage.py db upgrade
36 | $ python manage.py db migrate
37 | ```
38 |
39 | To update database after creating new migrations, use:
40 |
41 | ```sh
42 | $ python manage.py db upgrade
43 | ```
44 |
45 | ### Run Back-End
46 |
47 | ```sh
48 | $ python manage.py runserver
49 | ```
50 |
51 | ### Test Back-End
52 |
53 | ```sh
54 | $ python test.py --cov-report=term --cov-report=html --cov=application/ tests/
55 | ```
56 |
57 | ### Install Front-End Requirements
58 | ```sh
59 | $ cd react-ui
60 | $ yarn install
61 | ```
62 |
63 | ### Run Front-End
64 |
65 | ```sh
66 | $ cd react-ui
67 | $ yarn start
68 | ```
69 |
70 | ### New to Python?
71 |
72 | If you are approaching this demo as primarily a frontend dev with limited or no python experience, you may need to install a few things that a seasoned python dev would already have installed.
73 |
74 | Most Macs already have python 2.7 installed but you may not have pip install. You can check to see if you have them installed:
75 |
76 | ```
77 | $ python --version
78 | $ pip --version
79 | ```
80 |
81 | If pip is not installed, you can follow this simple article to [get both homebrew and python](https://howchoo.com/g/mze4ntbknjk/install-pip-on-mac-os-x)
82 |
83 | After you install python, you can optionally also install python 3
84 |
85 | ```
86 | $ brew install python3
87 | ```
88 |
89 | Now you can check again to see if both python and pip are installed. Once pip is installed, you can download the required flask modules:
90 |
91 | ```
92 | $ sudo pip install flask flask_script flask_migrate flask_bcrypt
93 | ```
94 |
95 | Now, you can decide on which database you wish to use.
96 |
97 | #### New to MySQL?
98 |
99 | If you decide on MySQL, install the free community edition of [MySQL](https://dev.mysql.com/downloads/mysql/) and [MySQL Workbench](https://www.mysql.com/products/workbench/)
100 |
101 | 1. start MySQL from the System Preferences
102 | 2. open MySQL Workbench and [create a database](http://stackoverflow.com/questions/5515745/create-a-new-database-with-mysql-workbench) called mydatabase but don't create the tables since python will do that for you
103 | 3. Install the MySQL connector for Python, add the DATABASE_URL configuration, and create the database and tables
104 |
105 | ```
106 | $ sudo pip install mysql-connector-python-rf
107 | $ export DATABASE_URL="mysql+mysqlconnector://username:password@localhost/mydatabase"
108 | $ python manage.py create_db
109 | ```
110 |
111 | Note: you do not need to run "python manage.py db upgrade" or "python manage.py db migrate" if its your first go at it
112 |
113 | 4. Run Back-End
114 |
115 | ```
116 | $ python manage.py runserver
117 | ```
118 |
119 | If all goes well, you should see ```* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)``` followed by a few more lines in the terminal.
120 |
121 | 5. open a new tab to the same directory and run the front end
122 |
123 | ```
124 | $ cd react-ui
125 | $ yarn install
126 | $ yarn start
127 | ```
128 |
129 | 6. open your browser to http://localhost:3000/register and setup your first account
130 | 7. enjoy! By this point, you should be able to create an account and login without errors.
131 |
--------------------------------------------------------------------------------
/react-ui/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
17 | );
18 |
19 | export default function register() {
20 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
21 | // The URL constructor is available in all browsers that support SW.
22 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
23 | if (publicUrl.origin !== window.location.origin) {
24 | // Our service worker won't work if PUBLIC_URL is on a different origin
25 | // from what our page is served on. This might happen if a CDN is used to
26 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
27 | return;
28 | }
29 |
30 | window.addEventListener('load', () => {
31 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
32 |
33 | if (!isLocalhost) {
34 | // Is not local host. Just register service worker
35 | registerValidSW(swUrl);
36 | } else {
37 | // This is running on localhost. Lets check if a service worker still exists or not.
38 | checkValidServiceWorker(swUrl);
39 | }
40 | });
41 | }
42 | }
43 |
44 | function registerValidSW(swUrl) {
45 | navigator.serviceWorker
46 | .register(swUrl)
47 | .then(registration => {
48 | registration.onupdatefound = () => {
49 | const installingWorker = registration.installing;
50 | installingWorker.onstatechange = () => {
51 | if (installingWorker.state === 'installed') {
52 | if (navigator.serviceWorker.controller) {
53 | // At this point, the old content will have been purged and
54 | // the fresh content will have been added to the cache.
55 | // It's the perfect time to display a "New content is
56 | // available; please refresh." message in your web app.
57 | console.log('New content is available; please refresh.');
58 | } else {
59 | // At this point, everything has been precached.
60 | // It's the perfect time to display a
61 | // "Content is cached for offline use." message.
62 | console.log('Content is cached for offline use.');
63 | }
64 | }
65 | };
66 | };
67 | })
68 | .catch(error => {
69 | console.error('Error during service worker registration:', error);
70 | });
71 | }
72 |
73 | function checkValidServiceWorker(swUrl) {
74 | // Check if the service worker can be found. If it can't reload the page.
75 | fetch(swUrl)
76 | .then(response => {
77 | // Ensure service worker exists, and that we really are getting a JS file.
78 | if (response.status === 404 || response.headers.get('content-type').indexOf('javascript') === -1) {
79 | // No service worker found. Probably a different app. Reload the page.
80 | navigator.serviceWorker.ready.then(registration => {
81 | registration.unregister().then(() => {
82 | window.location.reload();
83 | });
84 | });
85 | } else {
86 | // Service worker found. Proceed as normal.
87 | registerValidSW(swUrl);
88 | }
89 | })
90 | .catch(() => {
91 | console.log('No internet connection found. App is running in offline mode.');
92 | });
93 | }
94 |
95 | export function unregister() {
96 | if ('serviceWorker' in navigator) {
97 | navigator.serviceWorker.ready.then(registration => {
98 | registration.unregister();
99 | });
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/react-ui/src/components/RegisterView.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: 0, no-underscore-dangle: 0 */
2 |
3 | import React, { Component } from 'react';
4 | import PropTypes from 'prop-types';
5 | import { bindActionCreators } from 'redux';
6 | import { connect } from 'react-redux';
7 | import TextField from 'material-ui/TextField';
8 | import RaisedButton from 'material-ui/RaisedButton';
9 | import Paper from 'material-ui/Paper';
10 |
11 | import * as actionCreators from '../actions/auth';
12 |
13 | import { validateEmail } from '../utils/misc';
14 |
15 | const style = {
16 | marginTop: 50,
17 | paddingBottom: 50,
18 | paddingTop: 25,
19 | width: '100%',
20 | textAlign: 'center',
21 | display: 'inline-block'
22 | };
23 |
24 | class RegisterView extends Component {
25 | constructor(props) {
26 | super(props);
27 | const redirectRoute = '/login';
28 | this.state = {
29 | email: '',
30 | password: '',
31 | email_error_text: null,
32 | password_error_text: null,
33 | redirectTo: redirectRoute,
34 | disabled: true
35 | };
36 | }
37 |
38 | isDisabled() {
39 | let email_is_valid = false;
40 | let password_is_valid = false;
41 |
42 | if (this.state.email === '') {
43 | this.setState({ email_error_text: null });
44 | } else if (validateEmail(this.state.email)) {
45 | email_is_valid = true;
46 | this.setState({ email_error_text: null });
47 | } else {
48 | this.setState({ email_error_text: 'Sorry, this is not a valid email' });
49 | }
50 |
51 | if (this.state.password === '' || !this.state.password) {
52 | this.setState({ password_error_text: null });
53 | } else if (this.state.password.length >= 6) {
54 | password_is_valid = true;
55 | this.setState({ password_error_text: null });
56 | } else {
57 | this.setState({ password_error_text: 'Your password must be at least 6 characters' });
58 | }
59 |
60 | if (email_is_valid && password_is_valid) {
61 | this.setState({ disabled: false });
62 | }
63 | }
64 |
65 | changeValue(e, type) {
66 | const value = e.target.value;
67 | const next_state = {};
68 | next_state[type] = value;
69 | this.setState(next_state, () => {
70 | this.isDisabled();
71 | });
72 | }
73 |
74 | _handleKeyPress(e) {
75 | if (e.key === 'Enter') {
76 | if (!this.state.disabled) {
77 | this.login(e);
78 | }
79 | }
80 | }
81 |
82 | login(e) {
83 | e.preventDefault();
84 | this.props.registerUser(this.state.email, this.state.password, this.props.history);
85 | }
86 |
87 | render() {
88 | return (
89 | this._handleKeyPress(e)}>
90 |
91 |
92 |
Register to view protected content!
93 | {this.props.registerStatusText &&
{this.props.registerStatusText}
}
94 |
95 |
96 | this.changeValue(e, 'email')}
102 | />
103 |
104 |
105 | this.changeValue(e, 'password')}
111 | />
112 |
113 |
114 |
this.login(e)}
121 | />
122 |
123 |
124 |
125 | );
126 | }
127 | }
128 |
129 | RegisterView.propTypes = {
130 | registerUser: PropTypes.func,
131 | registerStatusText: PropTypes.string
132 | };
133 |
134 | function mapStateToProps(state) {
135 | return { isRegistering: state.auth.isRegistering, registerStatusText: state.auth.registerStatusText };
136 | }
137 |
138 | function mapDispatchToProps(dispatch) {
139 | return bindActionCreators(actionCreators, dispatch);
140 | }
141 |
142 | export default connect(
143 | mapStateToProps,
144 | mapDispatchToProps
145 | )(RegisterView);
146 |
--------------------------------------------------------------------------------
/react-ui/src/components/LoginView.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: 0, no-underscore-dangle: 0 */
2 |
3 | import React, { Component } from 'react';
4 | import PropTypes from 'prop-types';
5 | import { bindActionCreators } from 'redux';
6 | import { connect } from 'react-redux';
7 | import TextField from 'material-ui/TextField';
8 | import RaisedButton from 'material-ui/RaisedButton';
9 | import Paper from 'material-ui/Paper';
10 | import * as actionCreators from '../actions/auth';
11 | import { validateEmail } from '../utils/misc';
12 |
13 | const style = {
14 | marginTop: 50,
15 | paddingBottom: 50,
16 | paddingTop: 25,
17 | width: '100%',
18 | textAlign: 'center',
19 | display: 'inline-block'
20 | };
21 |
22 | class LoginView extends Component {
23 | constructor(props) {
24 | super(props);
25 | const redirectRoute = '/login';
26 | this.state = {
27 | email: '',
28 | password: '',
29 | email_error_text: null,
30 | password_error_text: null,
31 | redirectTo: redirectRoute,
32 | disabled: true
33 | };
34 | }
35 |
36 | isDisabled() {
37 | let email_is_valid = false;
38 | let password_is_valid = false;
39 |
40 | if (this.state.email === '') {
41 | this.setState({ email_error_text: null });
42 | } else if (validateEmail(this.state.email)) {
43 | email_is_valid = true;
44 | this.setState({ email_error_text: null });
45 | } else {
46 | this.setState({ email_error_text: 'Sorry, this is not a valid email' });
47 | }
48 |
49 | if (this.state.password === '' || !this.state.password) {
50 | this.setState({ password_error_text: null });
51 | } else if (this.state.password.length >= 6) {
52 | password_is_valid = true;
53 | this.setState({ password_error_text: null });
54 | } else {
55 | this.setState({ password_error_text: 'Your password must be at least 6 characters' });
56 | }
57 |
58 | if (email_is_valid && password_is_valid) {
59 | this.setState({ disabled: false });
60 | }
61 | }
62 |
63 | changeValue(e, type) {
64 | const value = e.target.value;
65 | const next_state = {};
66 | next_state[type] = value;
67 | this.setState(next_state, () => {
68 | this.isDisabled();
69 | });
70 | }
71 |
72 | _handleKeyPress(e) {
73 | if (e.key === 'Enter') {
74 | if (!this.state.disabled) {
75 | this.login(e);
76 | }
77 | }
78 | }
79 |
80 | login(e) {
81 | e.preventDefault();
82 | this.props.loginUser(this.state.email, this.state.password, this.props.history);
83 | }
84 |
85 | render() {
86 | return (
87 | this._handleKeyPress(e)}>
88 |
89 |
122 |
123 |
124 | );
125 | }
126 | }
127 |
128 | LoginView.propTypes = {
129 | loginUser: PropTypes.func,
130 | statusText: PropTypes.string
131 | };
132 |
133 | function mapStateToProps(state) {
134 | return { isAuthenticating: state.auth.isAuthenticating, statusText: state.auth.statusText };
135 | }
136 |
137 | function mapDispatchToProps(dispatch) {
138 | return bindActionCreators(actionCreators, dispatch);
139 | }
140 |
141 | export default connect(
142 | mapStateToProps,
143 | mapDispatchToProps
144 | )(LoginView);
145 |
--------------------------------------------------------------------------------