├── .editorconfig ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── __init__.py ├── application ├── __init__.py ├── app.py ├── models.py └── utils │ ├── __init__.py │ └── auth.py ├── basedir.py ├── config.py ├── index.py ├── main.py ├── manage.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 41c073a46b63_.py │ ├── 5aae5ada6624_.py │ └── ed657e16ce20_.py ├── requirements.txt ├── runtime.txt ├── setup.py ├── static ├── .babelrc ├── .eslintrc ├── bin │ └── server.js ├── bootstrap.rc ├── gh │ └── browser.png ├── index.html ├── karma.conf.js ├── package.json ├── server.js ├── src │ ├── actions │ │ ├── auth.js │ │ └── data.js │ ├── components │ │ ├── Analytics.js │ │ ├── AuthenticatedComponent.js │ │ ├── DetermineAuth.js │ │ ├── Footer │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── Header │ │ │ └── index.js │ │ ├── Home │ │ │ └── index.js │ │ ├── LoginView.js │ │ ├── NotFound.js │ │ ├── ProtectedView.js │ │ ├── RegisterView.js │ │ └── notAuthenticatedComponent.js │ ├── constants │ │ └── index.js │ ├── containers │ │ ├── App │ │ │ ├── index.js │ │ │ └── styles │ │ │ │ ├── app.scss │ │ │ │ ├── fonts │ │ │ │ └── roboto.scss │ │ │ │ ├── index.js │ │ │ │ ├── links.scss │ │ │ │ ├── screens.scss │ │ │ │ └── typography.scss │ │ └── HomeContainer │ │ │ └── index.js │ ├── index.js │ ├── reducers │ │ ├── auth.js │ │ ├── data.js │ │ └── index.js │ ├── routes.js │ ├── store │ │ └── configureStore.js │ ├── style.scss │ └── utils │ │ ├── http_functions.js │ │ ├── isMobileAndTablet.js │ │ ├── misc.js │ │ └── parallax.js └── webpack │ ├── common.config.js │ ├── dev.config.js │ └── prod.config.js ├── test.py ├── testing_config.py └── tests ├── test_api.py └── test_models.py /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | 8 | [package.json] 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | .DS_Store 3 | venv 4 | env 5 | __pycache__ 6 | blog_old.md 7 | node_modules 8 | .idea/* 9 | *.pyc 10 | tmp 11 | static/dist 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn main:app 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Redux-Flask # 2 | 3 | Boilerplate application for a Flask JWT Backend and a React/Redux Front-End with Material UI. 4 | 5 | * Python 2.7+ or 3.x 6 | * Pytest 7 | * Heroku 8 | * Flask 9 | * React 10 | * Redux 11 | * React-Router 2.0 12 | * React-Router-Redux 13 | * Babel 6 14 | * SCSS processing 15 | * Webpack 16 | 17 | ![screenshot](http://i.imgur.com/ZIS4qkw.png) 18 | 19 | ### Create DB 20 | ```sh 21 | $ export DATABASE_URL="postgresql://username:password@localhost/mydatabase" 22 | 23 | or 24 | 25 | $ export DATABASE_URL="mysql+mysqlconnector://username:password@localhost/mydatabase" 26 | 27 | or 28 | 29 | $ export DATABASE_URL="sqlite:///your.db" 30 | ``` 31 | (More about connection strings in this [flask config guide](http://flask-sqlalchemy.pocoo.org/2.1/config/).) 32 | ``` 33 | $ python manage.py create_db 34 | $ python manage.py db upgrade 35 | $ python manage.py db migrate 36 | ``` 37 | 38 | To update database after creating new migrations, use: 39 | 40 | ```sh 41 | $ python manage.py db upgrade 42 | ``` 43 | 44 | ### Install Front-End Requirements 45 | ```sh 46 | $ cd static 47 | $ npm install 48 | ``` 49 | 50 | ### Run Back-End 51 | 52 | ```sh 53 | $ python manage.py runserver 54 | ``` 55 | 56 | ### Test Back-End 57 | 58 | ```sh 59 | $ python test.py --cov-report=term --cov-report=html --cov=application/ tests/ 60 | ``` 61 | 62 | ### Run Front-End 63 | 64 | ```sh 65 | $ cd static 66 | $ npm start 67 | ``` 68 | 69 | ### Build Front-End 70 | 71 | ```sh 72 | $ npm run build:production 73 | ``` 74 | 75 | ### New to Python? 76 | 77 | 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. 78 | 79 | 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: 80 | 81 | ``` 82 | $ python --version 83 | $ pip --version 84 | ``` 85 | 86 | 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) 87 | 88 | After you install python, you can optionally also install python 3 89 | 90 | ``` 91 | $ brew install python3 92 | ``` 93 | 94 | 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: 95 | 96 | ``` 97 | $ sudo pip install flask flask_script flask_migrate flask_bcrypt 98 | ``` 99 | 100 | Now, you can decide on which database you wish to use. 101 | 102 | #### New to MySQL? 103 | 104 | 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/) 105 | 106 | 1. start MySQL from the System Preferences 107 | 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 108 | 3. Install the MySQL connector for Python, add the DATABASE_URL configuration, and create the database and tables 109 | 110 | ``` 111 | $ sudo pip install mysql-connector-python-rf 112 | $ export DATABASE_URL="mysql+mysqlconnector://username:password@localhost/mydatabase" 113 | $ python manage.py create_db 114 | ``` 115 | 116 | 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 117 | 118 | 4. Run Back-End 119 | 120 | ``` 121 | $ python manage.py runserver 122 | ``` 123 | 124 | 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. 125 | 126 | 5. open a new tab to the same directory and run the front end 127 | 128 | ``` 129 | $ cd static 130 | $ npm install 131 | $ npm start 132 | ``` 133 | 134 | 6. open your browser to http://localhost:3000/register and setup your first account 135 | 7. enjoy! By this point, you should be able to create an account and login without errors. 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dternyak/React-Redux-Flask/c547ca132c4ac4269850f4c813e9d4274156921b/__init__.py -------------------------------------------------------------------------------- /application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dternyak/React-Redux-Flask/c547ca132c4ac4269850f4c813e9d4274156921b/application/__init__.py -------------------------------------------------------------------------------- /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/get_token", methods=["POST"]) 47 | def get_token(): 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 | -------------------------------------------------------------------------------- /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).decode("utf-8") 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 | -------------------------------------------------------------------------------- /application/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dternyak/React-Redux-Flask/c547ca132c4ac4269850f4c813e9d4274156921b/application/utils/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /basedir.py: -------------------------------------------------------------------------------- 1 | import os 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | -------------------------------------------------------------------------------- /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 = os.environ['DATABASE_URL'] 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from application.app import app 2 | 3 | app = app 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.11.1 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.8 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.11.1 28 | six==1.10.0 29 | SQLAlchemy==1.0.14 30 | Werkzeug==0.11.10 31 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-2.7.9 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | -------------------------------------------------------------------------------- /static/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015" , "stage-0"], 3 | "plugins": [ 4 | ["transform-decorators-legacy"] 5 | ], 6 | "env": { 7 | "start": { 8 | "presets": ["react-hmre"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /static/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "extends": "eslint-config-airbnb", 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "globals": { 11 | "connect": true 12 | }, 13 | "rules": { 14 | "camelcase": 0, 15 | "indent": ["error", 4], 16 | "global-require": 0, 17 | "react/jsx-filename-extension": 0, 18 | "react/jsx-indent": ["error", 4], 19 | "import/prefer-default-export": 0, 20 | "import/no-extraneous-dependencies": 0, 21 | "import/no-unresolved": 0, 22 | "react/jsx-uses-react": 2, 23 | "react/jsx-uses-vars": 2, 24 | "react/react-in-jsx-scope": 2, 25 | "block-scoped-var": 0, 26 | "padded-blocks": 0, 27 | "no-console": 0, 28 | "id-length": 0, 29 | "no-unused-expressions": 0, 30 | }, 31 | "plugins": [ 32 | "react" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /static/bin/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var babelrc = fs.readFileSync('./.babelrc'); 4 | var config; 5 | 6 | try { 7 | config = JSON.parse(babelrc); 8 | } catch (err) { 9 | console.error('==> ERROR: Error parsing your .babelrc.'); 10 | console.error(err); 11 | } 12 | 13 | require('babel-core/register')(config); 14 | require('../server'); 15 | -------------------------------------------------------------------------------- /static/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 | -------------------------------------------------------------------------------- /static/gh/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dternyak/React-Redux-Flask/c547ca132c4ac4269850f4c813e9d4274156921b/static/gh/browser.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React-Redux-Flask 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /static/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: 'src', 4 | singleRun: true, 5 | frameworks: ['mocha'], 6 | reporters: ['dots'], 7 | browsers: ['Chrome'], 8 | files: [ 9 | 'test/**/*.spec.js', 10 | ], 11 | preprocessors: { 12 | 'test/**/*.spec.js': ['webpack'], 13 | }, 14 | webpack: { 15 | resolve: { 16 | extensions: ['', '.js', '.ts'], 17 | modulesDirectories: ['node_modules', 'src'], 18 | }, 19 | module: { 20 | loaders: [{ 21 | test: /\.js$/, 22 | loader: 'babel-loader', 23 | }], 24 | }, 25 | }, 26 | webpackMiddleware: { 27 | stats: { 28 | color: true, 29 | chunkModules: false, 30 | modules: false, 31 | }, 32 | }, 33 | }); 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-easy-boilerplate", 3 | "version": "1.3.3", 4 | "description": "", 5 | "scripts": { 6 | "clean": "rimraf dist", 7 | "build": "webpack --progress --verbose --colors --display-error-details --config webpack/common.config.js", 8 | "build:production": "npm run clean && npm run build", 9 | "lint": "eslint src", 10 | "start": "node bin/server.js", 11 | "test": "karma start" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "reactjs", 20 | "boilerplate", 21 | "redux", 22 | "hot", 23 | "reload", 24 | "hmr", 25 | "live", 26 | "edit", 27 | "webpack" 28 | ], 29 | "author": "https://github.com/anorudes, https://github.com/keske", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "autoprefixer": "6.5.3", 33 | "axios": "^0.15.3", 34 | "babel-core": "^6.4.5", 35 | "babel-eslint": "^7.1.1", 36 | "babel-loader": "^6.2.1", 37 | "babel-plugin-react-transform": "^2.0.0", 38 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 39 | "babel-polyfill": "^6.3.14", 40 | "babel-preset-es2015": "^6.3.13", 41 | "babel-preset-react": "^6.3.13", 42 | "babel-preset-react-hmre": "^1.0.1", 43 | "babel-preset-stage-0": "^6.3.13", 44 | "bootstrap": "^3.3.5", 45 | "bootstrap-loader": "^1.2.0-beta.1", 46 | "bootstrap-sass": "^3.3.6", 47 | "bootstrap-webpack": "0.0.5", 48 | "classnames": "^2.2.3", 49 | "css-loader": "^0.26.1", 50 | "csswring": "^5.1.0", 51 | "deep-equal": "^1.0.1", 52 | "eslint": "^3.4.0", 53 | "eslint-config-airbnb": "13.0.0", 54 | "eslint-plugin-import": "^2.2.0", 55 | "eslint-plugin-jsx-a11y": "^3.0.1", 56 | "eslint-plugin-react": "^6.1.2", 57 | "expect": "^1.13.4", 58 | "exports-loader": "^0.6.2", 59 | "expose-loader": "^0.7.1", 60 | "express": "^4.13.4", 61 | "express-open-in-editor": "^1.1.0", 62 | "extract-text-webpack-plugin": "^1.0.1", 63 | "file-loader": "^0.9.0", 64 | "gapi": "0.0.3", 65 | "history": "^4.4.1", 66 | "http-proxy": "^1.12.0", 67 | "imports-loader": "^0.6.5", 68 | "jasmine-core": "^2.4.1", 69 | "jquery": "^3.1.0", 70 | "jwt-decode": "^2.1.0", 71 | "karma": "^1.2.0", 72 | "karma-chrome-launcher": "^2.0.0", 73 | "karma-mocha": "^1.1.1", 74 | "karma-webpack": "^1.7.0", 75 | "less": "^2.5.3", 76 | "less-loader": "^2.2.2", 77 | "lodash": "^4.5.1", 78 | "material-ui": "^0.16.4", 79 | "mocha": "^3.0.2", 80 | "morgan": "^1.6.1", 81 | "node-sass": "^3.4.2", 82 | "postcss-import": "^9.0.0", 83 | "postcss-loader": "^1.1.1", 84 | "q": "^1.4.1", 85 | "qs": "^6.1.0", 86 | "rc-datepicker": "^4.0.1", 87 | "react": "^15.3.1", 88 | "react-addons-css-transition-group": "^15.3.1", 89 | "react-calendar-component": "^1.0.0", 90 | "react-date-picker": "^5.3.28", 91 | "react-datepicker": "^0.37.0", 92 | "react-document-meta": "^2.0.0-rc2", 93 | "react-dom": "^15.1.0", 94 | "react-forms": "^2.0.0-beta33", 95 | "react-hot-loader": "^1.3.0", 96 | "react-loading-order-with-animation": "^1.0.0", 97 | "react-onclickoutside": "^5.3.3", 98 | "react-redux": "^4.3.0", 99 | "react-router": "3.0.0", 100 | "react-router-redux": "^4.0.0", 101 | "react-tap-event-plugin": "^2.0.1", 102 | "react-transform-hmr": "^1.0.1", 103 | "redux": "^3.2.1", 104 | "redux-form": "^6.0.1", 105 | "redux-logger": "2.7.4", 106 | "redux-thunk": "^2.1.0", 107 | "resolve-url-loader": "^1.4.3", 108 | "rimraf": "^2.5.0", 109 | "sass-loader": "^4.0.0", 110 | "style-loader": "^0.13.0", 111 | "url-loader": "^0.5.7", 112 | "webpack": "^1.12.11", 113 | "webpack-dev-middleware": "^1.5.0", 114 | "webpack-dev-server": "^1.14.1", 115 | "webpack-hot-middleware": "^2.6.0", 116 | "webpack-merge": "^1.0.2", 117 | "yargs": "^6.5.0" 118 | }, 119 | "dependencies": {} 120 | } 121 | -------------------------------------------------------------------------------- /static/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const express = require('express'); 3 | const httpProxy = require('http-proxy'); 4 | const path = require('path'); 5 | 6 | const proxy = httpProxy.createProxyServer({}); 7 | 8 | const app = express(); 9 | 10 | app.use(require('morgan')('short')); 11 | 12 | (function initWebpack() { 13 | const webpack = require('webpack'); 14 | const webpackConfig = require('./webpack/common.config'); 15 | 16 | const compiler = webpack(webpackConfig); 17 | 18 | app.use(require('webpack-dev-middleware')(compiler, { 19 | noInfo: true, publicPath: webpackConfig.output.publicPath, 20 | })); 21 | 22 | app.use(require('webpack-hot-middleware')(compiler, { 23 | log: console.log, path: '/__webpack_hmr', heartbeat: 10 * 1000, 24 | })); 25 | 26 | app.use(express.static(path.join(__dirname, '/'))); 27 | }()); 28 | 29 | app.all(/^\/api\/(.*)/, (req, res) => { 30 | proxy.web(req, res, { target: 'http://localhost:5000' }); 31 | }); 32 | 33 | app.get(/.*/, (req, res) => { 34 | res.sendFile(path.join(__dirname, '/index.html')); 35 | }); 36 | 37 | 38 | const server = http.createServer(app); 39 | server.listen(process.env.PORT || 3000, () => { 40 | const address = server.address(); 41 | console.log('Listening on: %j', address); 42 | console.log(' -> that probably means: http://localhost:%d', address.port); 43 | }); 44 | -------------------------------------------------------------------------------- /static/src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { browserHistory } from 'react-router'; 2 | 3 | import { 4 | LOGIN_USER_SUCCESS, 5 | LOGIN_USER_FAILURE, 6 | LOGIN_USER_REQUEST, 7 | LOGOUT_USER, 8 | REGISTER_USER_FAILURE, 9 | REGISTER_USER_REQUEST, 10 | REGISTER_USER_SUCCESS, 11 | } from '../constants/index'; 12 | 13 | import { parseJSON } from '../utils/misc'; 14 | import { get_token, create_user } from '../utils/http_functions'; 15 | 16 | 17 | export function loginUserSuccess(token) { 18 | localStorage.setItem('token', token); 19 | return { 20 | type: LOGIN_USER_SUCCESS, 21 | payload: { 22 | token, 23 | }, 24 | }; 25 | } 26 | 27 | 28 | export function loginUserFailure(error) { 29 | localStorage.removeItem('token'); 30 | return { 31 | type: LOGIN_USER_FAILURE, 32 | payload: { 33 | status: error.response.status, 34 | statusText: error.response.statusText, 35 | }, 36 | }; 37 | } 38 | 39 | export function loginUserRequest() { 40 | return { 41 | type: LOGIN_USER_REQUEST, 42 | }; 43 | } 44 | 45 | export function logout() { 46 | localStorage.removeItem('token'); 47 | return { 48 | type: LOGOUT_USER, 49 | }; 50 | } 51 | 52 | export function logoutAndRedirect() { 53 | return (dispatch) => { 54 | dispatch(logout()); 55 | browserHistory.push('/'); 56 | }; 57 | } 58 | 59 | export function redirectToRoute(route) { 60 | return () => { 61 | browserHistory.push(route); 62 | }; 63 | } 64 | 65 | export function loginUser(email, password) { 66 | return function (dispatch) { 67 | dispatch(loginUserRequest()); 68 | return get_token(email, password) 69 | .then(parseJSON) 70 | .then(response => { 71 | try { 72 | dispatch(loginUserSuccess(response.token)); 73 | browserHistory.push('/main'); 74 | } catch (e) { 75 | alert(e); 76 | dispatch(loginUserFailure({ 77 | response: { 78 | status: 403, 79 | statusText: 'Invalid token', 80 | }, 81 | })); 82 | } 83 | }) 84 | .catch(error => { 85 | dispatch(loginUserFailure({ 86 | response: { 87 | status: 403, 88 | statusText: 'Invalid username or password', 89 | }, 90 | })); 91 | }); 92 | }; 93 | } 94 | 95 | 96 | export function registerUserRequest() { 97 | return { 98 | type: REGISTER_USER_REQUEST, 99 | }; 100 | } 101 | 102 | export function registerUserSuccess(token) { 103 | localStorage.setItem('token', token); 104 | return { 105 | type: REGISTER_USER_SUCCESS, 106 | payload: { 107 | token, 108 | }, 109 | }; 110 | } 111 | 112 | export function registerUserFailure(error) { 113 | localStorage.removeItem('token'); 114 | return { 115 | type: REGISTER_USER_FAILURE, 116 | payload: { 117 | status: error.response.status, 118 | statusText: error.response.statusText, 119 | }, 120 | }; 121 | } 122 | 123 | export function registerUser(email, password) { 124 | return function (dispatch) { 125 | dispatch(registerUserRequest()); 126 | return create_user(email, password) 127 | .then(parseJSON) 128 | .then(response => { 129 | try { 130 | dispatch(registerUserSuccess(response.token)); 131 | browserHistory.push('/main'); 132 | } catch (e) { 133 | dispatch(registerUserFailure({ 134 | response: { 135 | status: 403, 136 | statusText: 'Invalid token', 137 | }, 138 | })); 139 | } 140 | }) 141 | .catch(error => { 142 | dispatch(registerUserFailure({ 143 | response: { 144 | status: 403, 145 | statusText: 'User with that email already exists', 146 | }, 147 | } 148 | )); 149 | }); 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /static/src/actions/data.js: -------------------------------------------------------------------------------- 1 | import { FETCH_PROTECTED_DATA_REQUEST, RECEIVE_PROTECTED_DATA } from '../constants/index'; 2 | import { parseJSON } from '../utils/misc'; 3 | import { data_about_user } 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 function fetchProtectedData(token) { 22 | return (dispatch) => { 23 | dispatch(fetchProtectedDataRequest()); 24 | data_about_user(token) 25 | .then(parseJSON) 26 | .then(response => { 27 | dispatch(receiveProtectedData(response.result)); 28 | }) 29 | .catch(error => { 30 | if (error.status === 401) { 31 | dispatch(logoutAndRedirect(error)); 32 | } 33 | }); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /static/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 | function mapStateToProps(state) { 7 | return { 8 | isRegistering: state.auth.isRegistering, 9 | registerStatusText: state.auth.registerStatusText, 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return bindActionCreators(actionCreators, dispatch); 15 | } 16 | 17 | @connect(mapStateToProps, mapDispatchToProps) 18 | class Analytics extends React.Component { // eslint-disable-line react/prefer-stateless-function 19 | render() { 20 | return ( 21 |
22 |

Analytics

23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default Analytics; 30 | -------------------------------------------------------------------------------- /static/src/components/AuthenticatedComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { browserHistory } from 'react-router'; 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 | 20 | export function requireAuthentication(Component) { 21 | class AuthenticatedComponent extends React.Component { 22 | componentWillMount() { 23 | this.checkAuth(); 24 | this.state = { 25 | loaded_if_needed: false, 26 | }; 27 | } 28 | 29 | componentWillReceiveProps(nextProps) { 30 | this.checkAuth(nextProps); 31 | } 32 | 33 | checkAuth(props = this.props) { 34 | if (!props.isAuthenticated) { 35 | const token = localStorage.getItem('token'); 36 | if (!token) { 37 | browserHistory.push('/home'); 38 | } else { 39 | fetch('/api/is_token_valid', { 40 | method: 'post', 41 | credentials: 'include', 42 | headers: { 43 | 'Accept': 'application/json', // eslint-disable-line quote-props 44 | 'Content-Type': 'application/json', 45 | }, 46 | body: JSON.stringify({ token }), 47 | }) 48 | .then(res => { 49 | if (res.status === 200) { 50 | this.props.loginUserSuccess(token); 51 | this.setState({ 52 | loaded_if_needed: true, 53 | }); 54 | 55 | } else { 56 | browserHistory.push('/home'); 57 | 58 | } 59 | }); 60 | 61 | } 62 | } else { 63 | this.setState({ 64 | loaded_if_needed: true, 65 | }); 66 | } 67 | } 68 | 69 | render() { 70 | return ( 71 |
72 | {this.props.isAuthenticated && this.state.loaded_if_needed 73 | ? 74 | : null 75 | } 76 |
77 | ); 78 | 79 | } 80 | } 81 | 82 | AuthenticatedComponent.propTypes = { 83 | loginUserSuccess: React.PropTypes.func, 84 | isAuthenticated: React.PropTypes.bool, 85 | }; 86 | 87 | return connect(mapStateToProps, mapDispatchToProps)(AuthenticatedComponent); 88 | } 89 | -------------------------------------------------------------------------------- /static/src/components/DetermineAuth.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import * as actionCreators from '../actions/auth'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | token: state.auth.token, 9 | userName: state.auth.userName, 10 | isAuthenticated: state.auth.isAuthenticated, 11 | }; 12 | } 13 | 14 | function mapDispatchToProps(dispatch) { 15 | return bindActionCreators(actionCreators, dispatch); 16 | } 17 | 18 | 19 | export function DetermineAuth(Component) { 20 | 21 | class AuthenticatedComponent extends React.Component { 22 | 23 | componentWillMount() { 24 | this.checkAuth(); 25 | this.state = { 26 | loaded_if_needed: false, 27 | }; 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | this.checkAuth(nextProps); 32 | } 33 | 34 | checkAuth(props = this.props) { 35 | if (!props.isAuthenticated) { 36 | const token = localStorage.getItem('token'); 37 | if (token) { 38 | fetch('/api/is_token_valid', { 39 | method: 'post', 40 | credentials: 'include', 41 | headers: { 42 | 'Accept': 'application/json', // eslint-disable-line quote-props 43 | 'Content-Type': 'application/json', 44 | }, 45 | body: JSON.stringify({ token }), 46 | }) 47 | .then(res => { 48 | if (res.status === 200) { 49 | this.props.loginUserSuccess(token); 50 | this.setState({ 51 | loaded_if_needed: true, 52 | }); 53 | 54 | } 55 | }); 56 | } 57 | 58 | } else { 59 | this.setState({ 60 | loaded_if_needed: true, 61 | }); 62 | } 63 | } 64 | 65 | render() { 66 | return ( 67 |
68 | {this.state.loaded_if_needed 69 | ? 70 | : null 71 | } 72 |
73 | ); 74 | 75 | } 76 | } 77 | 78 | AuthenticatedComponent.propTypes = { 79 | loginUserSuccess: React.PropTypes.func, 80 | }; 81 | 82 | return connect(mapStateToProps, mapDispatchToProps)(AuthenticatedComponent); 83 | 84 | } 85 | -------------------------------------------------------------------------------- /static/src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* component styles */ 4 | import { styles } from './styles.scss'; 5 | 6 | export const Footer = () => 7 | ; 16 | -------------------------------------------------------------------------------- /static/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: absolute; 8 | bottom: -70; 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /static/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | import AppBar from 'material-ui/AppBar'; 6 | import LeftNav from 'material-ui/Drawer'; 7 | import MenuItem from 'material-ui/MenuItem'; 8 | import FlatButton from 'material-ui/FlatButton'; 9 | import Divider from 'material-ui/Divider'; 10 | 11 | import * as actionCreators from '../../actions/auth'; 12 | 13 | function mapStateToProps(state) { 14 | return { 15 | token: state.auth.token, 16 | userName: state.auth.userName, 17 | isAuthenticated: state.auth.isAuthenticated, 18 | }; 19 | } 20 | 21 | function mapDispatchToProps(dispatch) { 22 | return bindActionCreators(actionCreators, dispatch); 23 | } 24 | 25 | @connect(mapStateToProps, mapDispatchToProps) 26 | export class Header extends Component { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | open: false, 31 | }; 32 | 33 | } 34 | 35 | dispatchNewRoute(route) { 36 | browserHistory.push(route); 37 | this.setState({ 38 | open: false, 39 | }); 40 | 41 | } 42 | 43 | 44 | handleClickOutside() { 45 | this.setState({ 46 | open: false, 47 | }); 48 | } 49 | 50 | 51 | logout(e) { 52 | e.preventDefault(); 53 | this.props.logoutAndRedirect(); 54 | this.setState({ 55 | open: false, 56 | }); 57 | } 58 | 59 | openNav() { 60 | this.setState({ 61 | open: true, 62 | }); 63 | } 64 | 65 | render() { 66 | return ( 67 |
68 | 69 | { 70 | !this.props.isAuthenticated ? 71 |
72 | this.dispatchNewRoute('/login')}> 73 | Login 74 | 75 | this.dispatchNewRoute('/register')}> 76 | Register 77 | 78 |
79 | : 80 |
81 | this.dispatchNewRoute('/analytics')}> 82 | Analytics 83 | 84 | 85 | 86 | this.logout(e)}> 87 | Logout 88 | 89 |
90 | } 91 |
92 | this.openNav()} 95 | iconElementRight={ 96 | this.dispatchNewRoute('/')} /> 97 | } 98 | /> 99 |
100 | 101 | ); 102 | } 103 | } 104 | 105 | Header.propTypes = { 106 | logoutAndRedirect: React.PropTypes.func, 107 | isAuthenticated: React.PropTypes.bool, 108 | }; 109 | -------------------------------------------------------------------------------- /static/src/components/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Home = () => 4 |
5 |
6 |

Hello

7 |
8 |
; 9 | -------------------------------------------------------------------------------- /static/src/components/LoginView.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0, no-underscore-dangle: 0 */ 2 | 3 | import React from 'react'; 4 | import { bindActionCreators } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import TextField from 'material-ui/TextField'; 7 | import RaisedButton from 'material-ui/RaisedButton'; 8 | import Paper from 'material-ui/Paper'; 9 | import * as actionCreators from '../actions/auth'; 10 | import { validateEmail } from '../utils/misc'; 11 | 12 | function mapStateToProps(state) { 13 | return { 14 | isAuthenticating: state.auth.isAuthenticating, 15 | statusText: state.auth.statusText, 16 | }; 17 | } 18 | 19 | function mapDispatchToProps(dispatch) { 20 | return bindActionCreators(actionCreators, dispatch); 21 | } 22 | 23 | 24 | const style = { 25 | marginTop: 50, 26 | paddingBottom: 50, 27 | paddingTop: 25, 28 | width: '100%', 29 | textAlign: 'center', 30 | display: 'inline-block', 31 | }; 32 | 33 | @connect(mapStateToProps, mapDispatchToProps) 34 | export default class LoginView extends React.Component { 35 | 36 | constructor(props) { 37 | super(props); 38 | const redirectRoute = '/login'; 39 | this.state = { 40 | email: '', 41 | password: '', 42 | email_error_text: null, 43 | password_error_text: null, 44 | redirectTo: redirectRoute, 45 | disabled: true, 46 | }; 47 | } 48 | 49 | isDisabled() { 50 | let email_is_valid = false; 51 | let password_is_valid = false; 52 | 53 | if (this.state.email === '') { 54 | this.setState({ 55 | email_error_text: null, 56 | }); 57 | } else if (validateEmail(this.state.email)) { 58 | email_is_valid = true; 59 | this.setState({ 60 | email_error_text: null, 61 | }); 62 | 63 | } else { 64 | this.setState({ 65 | email_error_text: 'Sorry, this is not a valid email', 66 | }); 67 | } 68 | 69 | if (this.state.password === '' || !this.state.password) { 70 | this.setState({ 71 | password_error_text: null, 72 | }); 73 | } else if (this.state.password.length >= 6) { 74 | password_is_valid = true; 75 | this.setState({ 76 | password_error_text: null, 77 | }); 78 | } else { 79 | this.setState({ 80 | password_error_text: 'Your password must be at least 6 characters', 81 | }); 82 | 83 | } 84 | 85 | if (email_is_valid && password_is_valid) { 86 | this.setState({ 87 | disabled: false, 88 | }); 89 | } 90 | 91 | } 92 | 93 | changeValue(e, type) { 94 | const value = e.target.value; 95 | const next_state = {}; 96 | next_state[type] = value; 97 | this.setState(next_state, () => { 98 | this.isDisabled(); 99 | }); 100 | } 101 | 102 | _handleKeyPress(e) { 103 | if (e.key === 'Enter') { 104 | if (!this.state.disabled) { 105 | this.login(e); 106 | } 107 | } 108 | } 109 | 110 | login(e) { 111 | e.preventDefault(); 112 | this.props.loginUser(this.state.email, this.state.password, this.state.redirectTo); 113 | } 114 | 115 | render() { 116 | return ( 117 |
this._handleKeyPress(e)}> 118 | 119 |
120 |
121 |

Login to view protected content!

122 | { 123 | this.props.statusText && 124 |
125 | {this.props.statusText} 126 |
127 | } 128 | 129 |
130 | this.changeValue(e, 'email')} 136 | /> 137 |
138 |
139 | this.changeValue(e, 'password')} 145 | /> 146 |
147 | 148 | this.login(e)} 153 | /> 154 | 155 |
156 |
157 |
158 | 159 |
160 | ); 161 | 162 | } 163 | } 164 | 165 | LoginView.propTypes = { 166 | loginUser: React.PropTypes.func, 167 | statusText: React.PropTypes.string, 168 | }; 169 | -------------------------------------------------------------------------------- /static/src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import * as actionCreators from '../actions/auth'; 5 | 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 | @connect(mapStateToProps, mapDispatchToProps) 20 | class NotFound extends React.Component { // eslint-disable-line react/prefer-stateless-function 21 | render() { 22 | return ( 23 |
24 |

Not Found

25 |
26 | ); 27 | } 28 | } 29 | 30 | export default NotFound; 31 | -------------------------------------------------------------------------------- /static/src/components/ProtectedView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import * as actionCreators from '../actions/data'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | data: state.data, 9 | token: state.auth.token, 10 | loaded: state.data.loaded, 11 | isFetching: state.data.isFetching, 12 | }; 13 | } 14 | 15 | 16 | function mapDispatchToProps(dispatch) { 17 | return bindActionCreators(actionCreators, dispatch); 18 | } 19 | 20 | @connect(mapStateToProps, mapDispatchToProps) 21 | export default class ProtectedView extends React.Component { 22 | componentDidMount() { 23 | this.fetchData(); 24 | } 25 | 26 | 27 | fetchData() { 28 | const token = this.props.token; 29 | this.props.fetchProtectedData(token); 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 | {!this.props.loaded 36 | ?

Loading data...

37 | : 38 |
39 |

Welcome back, 40 | {this.props.userName}!

41 |

{this.props.data.data.email}

42 |
43 | } 44 |
45 | ); 46 | } 47 | } 48 | 49 | ProtectedView.propTypes = { 50 | fetchProtectedData: React.PropTypes.func, 51 | loaded: React.PropTypes.bool, 52 | userName: React.PropTypes.string, 53 | data: React.PropTypes.any, 54 | token: React.PropTypes.string, 55 | }; 56 | -------------------------------------------------------------------------------- /static/src/components/RegisterView.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0, no-underscore-dangle: 0 */ 2 | 3 | import React from 'react'; 4 | import { bindActionCreators } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import TextField from 'material-ui/TextField'; 7 | import RaisedButton from 'material-ui/RaisedButton'; 8 | import Paper from 'material-ui/Paper'; 9 | 10 | import * as actionCreators from '../actions/auth'; 11 | 12 | import { validateEmail } from '../utils/misc'; 13 | 14 | function mapStateToProps(state) { 15 | return { 16 | isRegistering: state.auth.isRegistering, 17 | registerStatusText: state.auth.registerStatusText, 18 | }; 19 | } 20 | 21 | function mapDispatchToProps(dispatch) { 22 | return bindActionCreators(actionCreators, dispatch); 23 | } 24 | 25 | const style = { 26 | marginTop: 50, 27 | paddingBottom: 50, 28 | paddingTop: 25, 29 | width: '100%', 30 | textAlign: 'center', 31 | display: 'inline-block', 32 | }; 33 | 34 | @connect(mapStateToProps, mapDispatchToProps) 35 | export default class RegisterView extends React.Component { 36 | 37 | constructor(props) { 38 | super(props); 39 | const redirectRoute = '/login'; 40 | this.state = { 41 | email: '', 42 | password: '', 43 | email_error_text: null, 44 | password_error_text: null, 45 | redirectTo: redirectRoute, 46 | disabled: true, 47 | }; 48 | } 49 | 50 | isDisabled() { 51 | let email_is_valid = false; 52 | let password_is_valid = false; 53 | 54 | if (this.state.email === '') { 55 | this.setState({ 56 | email_error_text: null, 57 | }); 58 | } else if (validateEmail(this.state.email)) { 59 | email_is_valid = true; 60 | this.setState({ 61 | email_error_text: null, 62 | }); 63 | 64 | } else { 65 | this.setState({ 66 | email_error_text: 'Sorry, this is not a valid email', 67 | }); 68 | } 69 | 70 | if (this.state.password === '' || !this.state.password) { 71 | this.setState({ 72 | password_error_text: null, 73 | }); 74 | } else if (this.state.password.length >= 6) { 75 | password_is_valid = true; 76 | this.setState({ 77 | password_error_text: null, 78 | }); 79 | } else { 80 | this.setState({ 81 | password_error_text: 'Your password must be at least 6 characters', 82 | }); 83 | 84 | } 85 | 86 | if (email_is_valid && password_is_valid) { 87 | this.setState({ 88 | disabled: false, 89 | }); 90 | } 91 | 92 | } 93 | 94 | changeValue(e, type) { 95 | const value = e.target.value; 96 | const next_state = {}; 97 | next_state[type] = value; 98 | this.setState(next_state, () => { 99 | this.isDisabled(); 100 | }); 101 | } 102 | 103 | _handleKeyPress(e) { 104 | if (e.key === 'Enter') { 105 | if (!this.state.disabled) { 106 | this.login(e); 107 | } 108 | } 109 | } 110 | 111 | login(e) { 112 | e.preventDefault(); 113 | this.props.registerUser(this.state.email, this.state.password, this.state.redirectTo); 114 | } 115 | 116 | render() { 117 | return ( 118 |
this._handleKeyPress(e)}> 119 | 120 |
121 |

Register to view protected content!

122 | { 123 | this.props.registerStatusText && 124 |
125 | {this.props.registerStatusText} 126 |
127 | } 128 | 129 |
130 | this.changeValue(e, 'email')} 136 | /> 137 |
138 |
139 | this.changeValue(e, 'password')} 145 | /> 146 |
147 | 148 | this.login(e)} 153 | /> 154 | 155 |
156 |
157 | 158 |
159 | ); 160 | 161 | } 162 | } 163 | 164 | RegisterView.propTypes = { 165 | registerUser: React.PropTypes.func, 166 | registerStatusText: React.PropTypes.string, 167 | }; 168 | -------------------------------------------------------------------------------- /static/src/components/notAuthenticatedComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { browserHistory } from 'react-router'; 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 | 20 | export function requireNoAuthentication(Component) { 21 | 22 | class notAuthenticatedComponent extends React.Component { 23 | 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | loaded: false, 28 | }; 29 | } 30 | 31 | componentWillMount() { 32 | this.checkAuth(); 33 | } 34 | 35 | componentWillReceiveProps(nextProps) { 36 | this.checkAuth(nextProps); 37 | } 38 | 39 | checkAuth(props = this.props) { 40 | if (props.isAuthenticated) { 41 | browserHistory.push('/main'); 42 | 43 | } else { 44 | const token = localStorage.getItem('token'); 45 | if (token) { 46 | fetch('/api/is_token_valid', { 47 | method: 'post', 48 | credentials: 'include', 49 | headers: { 50 | 'Accept': 'application/json', // eslint-disable-line quote-props 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify({ token }), 54 | }) 55 | .then(res => { 56 | if (res.status === 200) { 57 | this.props.loginUserSuccess(token); 58 | browserHistory.push('/main'); 59 | 60 | } else { 61 | this.setState({ 62 | loaded: true, 63 | }); 64 | } 65 | }); 66 | } else { 67 | this.setState({ 68 | loaded: true, 69 | }); 70 | } 71 | } 72 | } 73 | 74 | render() { 75 | return ( 76 |
77 | {!this.props.isAuthenticated && this.state.loaded 78 | ? 79 | : null 80 | } 81 |
82 | ); 83 | 84 | } 85 | } 86 | 87 | notAuthenticatedComponent.propTypes = { 88 | loginUserSuccess: React.PropTypes.func, 89 | isAuthenticated: React.PropTypes.bool, 90 | }; 91 | 92 | return connect(mapStateToProps, mapDispatchToProps)(notAuthenticatedComponent); 93 | 94 | } 95 | -------------------------------------------------------------------------------- /static/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 | -------------------------------------------------------------------------------- /static/src/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | 6 | /* application components */ 7 | import { Header } from '../../components/Header'; 8 | import { Footer } from '../../components/Footer'; 9 | 10 | /* global styles for app */ 11 | import './styles/app.scss'; 12 | 13 | class App extends React.Component { // eslint-disable-line react/prefer-stateless-function 14 | static propTypes = { 15 | children: React.PropTypes.node, 16 | }; 17 | 18 | render() { 19 | return ( 20 | 21 |
22 |
23 |
27 | {this.props.children} 28 |
29 |
30 |
31 |
32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export { App }; 39 | -------------------------------------------------------------------------------- /static/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 | -------------------------------------------------------------------------------- /static/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 | -------------------------------------------------------------------------------- /static/src/containers/App/styles/index.js: -------------------------------------------------------------------------------- 1 | import 'style!./styles.scss'; 2 | export default require('./styles.scss').locals.styles; 3 | -------------------------------------------------------------------------------- /static/src/containers/App/styles/links.scss: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | 4 | &:hover { 5 | text-decoration: none; 6 | } 7 | } -------------------------------------------------------------------------------- /static/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"; -------------------------------------------------------------------------------- /static/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 | } -------------------------------------------------------------------------------- /static/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 |
8 | 9 |
; 10 | -------------------------------------------------------------------------------- /static/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, Redirect, browserHistory } from 'react-router'; 5 | import injectTapEventPlugin from 'react-tap-event-plugin'; 6 | import { syncHistoryWithStore } from 'react-router-redux'; 7 | 8 | import configureStore from './store/configureStore'; 9 | import routes from './routes'; 10 | import './style.scss'; 11 | 12 | require('expose?$!expose?jQuery!jquery'); 13 | require('bootstrap-webpack'); 14 | 15 | injectTapEventPlugin(); 16 | const store = configureStore(); 17 | const history = syncHistoryWithStore(browserHistory, store); 18 | 19 | ReactDOM.render( 20 | 21 | 22 | 23 | {routes} 24 | 25 | , 26 | document.getElementById('root') 27 | ); 28 | -------------------------------------------------------------------------------- /static/src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import jwtDecode from 'jwt-decode'; 2 | 3 | import { createReducer } from '../utils/misc'; 4 | import { 5 | LOGIN_USER_SUCCESS, 6 | LOGIN_USER_FAILURE, 7 | LOGIN_USER_REQUEST, 8 | LOGOUT_USER, 9 | REGISTER_USER_FAILURE, 10 | REGISTER_USER_REQUEST, 11 | REGISTER_USER_SUCCESS, 12 | } from '../constants/index'; 13 | 14 | const initialState = { 15 | token: null, 16 | userName: null, 17 | isAuthenticated: false, 18 | isAuthenticating: false, 19 | statusText: null, 20 | isRegistering: false, 21 | isRegistered: false, 22 | registerStatusText: null, 23 | }; 24 | 25 | export default createReducer(initialState, { 26 | [LOGIN_USER_REQUEST]: (state) => 27 | Object.assign({}, state, { 28 | isAuthenticating: true, 29 | statusText: null, 30 | }), 31 | [LOGIN_USER_SUCCESS]: (state, payload) => 32 | Object.assign({}, state, { 33 | isAuthenticating: false, 34 | isAuthenticated: true, 35 | token: payload.token, 36 | userName: jwtDecode(payload.token).email, 37 | statusText: 'You have been successfully logged in.', 38 | }), 39 | [LOGIN_USER_FAILURE]: (state, payload) => 40 | Object.assign({}, state, { 41 | isAuthenticating: false, 42 | isAuthenticated: false, 43 | token: null, 44 | userName: null, 45 | statusText: `Authentication Error: ${payload.status} ${payload.statusText}`, 46 | }), 47 | [LOGOUT_USER]: (state) => 48 | Object.assign({}, state, { 49 | isAuthenticated: false, 50 | token: null, 51 | userName: null, 52 | statusText: 'You have been successfully logged out.', 53 | }), 54 | [REGISTER_USER_SUCCESS]: (state, payload) => 55 | Object.assign({}, state, { 56 | isAuthenticating: false, 57 | isAuthenticated: true, 58 | isRegistering: false, 59 | token: payload.token, 60 | userName: jwtDecode(payload.token).email, 61 | registerStatusText: 'You have been successfully logged in.', 62 | }), 63 | [REGISTER_USER_REQUEST]: (state) => 64 | Object.assign({}, state, { 65 | isRegistering: true, 66 | }), 67 | [REGISTER_USER_FAILURE]: (state, payload) => 68 | Object.assign({}, state, { 69 | isAuthenticated: false, 70 | token: null, 71 | userName: null, 72 | registerStatusText: `Register Error: ${payload.status} ${payload.statusText}`, 73 | }), 74 | }); 75 | -------------------------------------------------------------------------------- /static/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 | -------------------------------------------------------------------------------- /static/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 | -------------------------------------------------------------------------------- /static/src/routes.js: -------------------------------------------------------------------------------- 1 | /* eslint new-cap: 0 */ 2 | 3 | import React from 'react'; 4 | import { Route } from 'react-router'; 5 | 6 | /* containers */ 7 | import { App } from './containers/App'; 8 | import { HomeContainer } from './containers/HomeContainer'; 9 | import LoginView from './components/LoginView'; 10 | import RegisterView from './components/RegisterView'; 11 | import ProtectedView from './components/ProtectedView'; 12 | import Analytics from './components/Analytics'; 13 | import NotFound from './components/NotFound'; 14 | 15 | import { DetermineAuth } from './components/DetermineAuth'; 16 | import { requireAuthentication } from './components/AuthenticatedComponent'; 17 | import { requireNoAuthentication } from './components/notAuthenticatedComponent'; 18 | 19 | export default ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /static/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | const debugware = []; 6 | if (process.env.NODE_ENV !== 'production') { 7 | const createLogger = require('redux-logger'); 8 | 9 | debugware.push(createLogger({ 10 | collapsed: true, 11 | })); 12 | } 13 | 14 | export default function configureStore(initialState) { 15 | const store = createStore( 16 | rootReducer, 17 | initialState, 18 | applyMiddleware(thunkMiddleware, ...debugware) 19 | ); 20 | 21 | if (module.hot) { 22 | // Enable Webpack hot module replacement for reducers 23 | module.hot.accept('../reducers', () => { 24 | const nextRootReducer = require('../reducers/index').default; 25 | 26 | store.replaceReducer(nextRootReducer); 27 | }); 28 | } 29 | 30 | return store; 31 | } 32 | -------------------------------------------------------------------------------- /static/src/style.scss: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | position: relative; 3 | 4 | 5 | } 6 | -------------------------------------------------------------------------------- /static/src/utils/http_functions.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | 3 | import axios from 'axios'; 4 | 5 | const tokenConfig = (token) => ({ 6 | headers: { 7 | 'Authorization': token, // eslint-disable-line quote-props 8 | }, 9 | }); 10 | 11 | export function validate_token(token) { 12 | return axios.post('/api/is_token_valid', { 13 | token, 14 | }); 15 | } 16 | 17 | export function get_github_access() { 18 | window.open( 19 | '/github-login', 20 | '_blank' // <- This is what makes it open in a new window. 21 | ); 22 | } 23 | 24 | export function create_user(email, password) { 25 | return axios.post('/api/create_user', { 26 | email, 27 | password, 28 | }); 29 | } 30 | 31 | export function get_token(email, password) { 32 | return axios.post('/api/get_token', { 33 | email, 34 | password, 35 | }); 36 | } 37 | 38 | export function has_github_token(token) { 39 | return axios.get('/api/has_github_token', tokenConfig(token)); 40 | } 41 | 42 | export function data_about_user(token) { 43 | return axios.get('/api/user', tokenConfig(token)); 44 | } 45 | -------------------------------------------------------------------------------- /static/src/utils/isMobileAndTablet.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Is mobile or tablet? 3 | * 4 | * @return {Boolean} 5 | */ 6 | export function isMobileAndTablet() { 7 | return window.innerWidth <= 800 && window.innerHeight <= 600; 8 | } 9 | -------------------------------------------------------------------------------- /static/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 | 15 | return reducer 16 | ? reducer(state, action.payload) 17 | : state; 18 | }; 19 | } 20 | 21 | 22 | export function parseJSON(response) { 23 | return response.data; 24 | } 25 | 26 | export function validateEmail(email) { 27 | 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,}))$/; 28 | return re.test(email); 29 | } 30 | -------------------------------------------------------------------------------- /static/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 | -------------------------------------------------------------------------------- /static/webpack/common.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const autoprefixer = require('autoprefixer'); 3 | const postcssImport = require('postcss-import'); 4 | const merge = require('webpack-merge'); 5 | 6 | const development = require('./dev.config'); 7 | const production = require('./prod.config'); 8 | 9 | require('babel-polyfill').default; 10 | 11 | const TARGET = process.env.npm_lifecycle_event; 12 | 13 | const PATHS = { 14 | app: path.join(__dirname, '../src'), 15 | build: path.join(__dirname, '../dist'), 16 | }; 17 | 18 | process.env.BABEL_ENV = TARGET; 19 | 20 | const common = { 21 | entry: [ 22 | PATHS.app, 23 | ], 24 | 25 | output: { 26 | path: PATHS.build, 27 | filename: 'bundle.js', 28 | }, 29 | 30 | resolve: { 31 | extensions: ['', '.jsx', '.js', '.json', '.scss'], 32 | modulesDirectories: ['node_modules', PATHS.app], 33 | }, 34 | 35 | module: { 36 | loaders: [{ 37 | test: /bootstrap-sass\/assets\/javascripts\//, 38 | loader: 'imports?jQuery=jquery', 39 | }, { 40 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 41 | loader: 'url?limit=10000&mimetype=application/font-woff', 42 | }, { 43 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 44 | loader: 'url?limit=10000&mimetype=application/font-woff2', 45 | }, { 46 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 47 | loader: 'url?limit=10000&mimetype=application/octet-stream', 48 | }, { 49 | test: /\.otf(\?v=\d+\.\d+\.\d+)?$/, 50 | loader: 'url?limit=10000&mimetype=application/font-otf', 51 | }, { 52 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 53 | loader: 'file', 54 | }, { 55 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 56 | loader: 'url?limit=10000&mimetype=image/svg+xml', 57 | }, { 58 | test: /\.js$/, 59 | loaders: ['babel-loader'], 60 | exclude: /node_modules/, 61 | }, { 62 | test: /\.png$/, 63 | loader: 'file?name=[name].[ext]', 64 | }, { 65 | test: /\.jpg$/, 66 | loader: 'file?name=[name].[ext]', 67 | }], 68 | }, 69 | 70 | postcss: (webpack) => ( 71 | [ 72 | autoprefixer({ 73 | browsers: ['last 2 versions'], 74 | }), 75 | postcssImport({ 76 | addDependencyTo: webpack, 77 | }), 78 | ] 79 | ), 80 | }; 81 | 82 | if (TARGET === 'start' || !TARGET) { 83 | module.exports = merge(development, common); 84 | } 85 | 86 | if (TARGET === 'build' || !TARGET) { 87 | module.exports = merge(production, common); 88 | } 89 | -------------------------------------------------------------------------------- /static/webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'bootstrap-loader', 8 | 'webpack-hot-middleware/client', 9 | './src/index', 10 | ], 11 | output: { 12 | publicPath: '/dist/', 13 | }, 14 | 15 | module: { 16 | loaders: [{ 17 | test: /\.scss$/, 18 | loader: 'style!css?localIdentName=[path][name]--[local]!postcss-loader!sass', 19 | }], 20 | }, 21 | 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': { 25 | NODE_ENV: '"development"', 26 | }, 27 | __DEVELOPMENT__: true, 28 | }), 29 | new ExtractTextPlugin('bundle.css'), 30 | new webpack.optimize.OccurenceOrderPlugin(), 31 | new webpack.HotModuleReplacementPlugin(), 32 | new webpack.NoErrorsPlugin(), 33 | new webpack.ProvidePlugin({ 34 | jQuery: 'jquery', 35 | }), 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /static/webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | 7 | entry: ['bootstrap-loader/extractStyles'], 8 | 9 | output: { 10 | publicPath: 'dist/', 11 | }, 12 | 13 | module: { 14 | loaders: [{ 15 | test: /\.scss$/, 16 | loader: 'style!css!postcss-loader!sass', 17 | }], 18 | }, 19 | 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': { 23 | NODE_ENV: '"production"', 24 | }, 25 | __DEVELOPMENT__: false, 26 | }), 27 | new ExtractTextPlugin('bundle.css'), 28 | new webpack.optimize.DedupePlugin(), 29 | new webpack.optimize.OccurenceOrderPlugin(), 30 | new webpack.optimize.UglifyJsPlugin({ 31 | compress: { 32 | warnings: false, 33 | }, 34 | }), 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_get_token_and_verify_token(self): 40 | res = self.app.post( 41 | "/api/get_token", 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/get_token", 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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------