├── .gitignore ├── .python-version ├── .tool-versions ├── Readme.md ├── api ├── .dockerignore ├── .gitignore ├── Dockerfile ├── Dockerfile-prod ├── index.py ├── manage.py ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── 94c5bdd99d14_initial_migration.py ├── now.json ├── project │ ├── __init__.py │ ├── config.py │ ├── db │ │ ├── Dockerfile │ │ └── create.sql │ ├── extensions │ │ ├── __init__.py │ │ └── logging.py │ └── modules │ │ ├── __init__.py │ │ ├── auth │ │ ├── __init__.py │ │ └── registration.py │ │ ├── users │ │ ├── __init__.py │ │ └── models.py │ │ └── utils.py ├── requirements-dev.txt ├── requirements.txt └── setup.cfg ├── client ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── components │ ├── error-boundary.tsx │ ├── errors │ │ ├── five_hundred_error.tsx │ │ ├── four_zero_four_error.tsx │ │ └── index.ts │ ├── layout │ │ ├── footer.tsx │ │ ├── header.tsx │ │ ├── index.ts │ │ └── menu.tsx │ └── text-input.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ ├── index.tsx │ ├── login.tsx │ ├── profile.tsx │ ├── register.tsx │ └── secure-page.tsx ├── services │ ├── api │ │ ├── api-config.ts │ │ ├── api-problem.ts │ │ ├── api.ts │ │ └── index.ts │ ├── auth.ts │ ├── index.ts │ ├── types.ts │ └── users.ts ├── static │ └── styles │ │ └── style.css ├── tsconfig.json ├── utils │ └── auth │ │ ├── auth-hook.tsx │ │ ├── auth-token.ts │ │ ├── auth.tsx │ │ ├── index.ts │ │ └── redirect.tsx └── yarn.lock └── docker-compose.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | tags 3 | .tern_port 4 | tags.* 5 | node_modules 6 | desktop.ini 7 | .vscode 8 | .vim 9 | data 10 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | user-management-example 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 13.13.0 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Flask-React-Docker App 2 | 3 | An example of web application using Flask, React + NextJS with Docker. 4 | 5 | ## Getting Started 6 | 7 | This project consists of Flask application for the backend API, NextJS for client side application and nginx as a reverse-proxy for connecting api and the front-end. This project also use `docker-compose` to make it easy run all of the container at once. 8 | 9 | This application will showcase: 10 | 11 | - Flask application with Users and Auth endpoint 12 | - NextJS application that showing normal route and authenticated routes. 13 | 14 | ### Prerequisites 15 | 16 | Before you run this application make sure you have this installed in your machine: 17 | 18 | - [Docker Desktop](https://www.docker.com/products/docker-desktop) 19 | - [docker-compose](https://docs.docker.com/compose/install/) 20 | 21 | ### Running Locally 22 | 23 | To run the application locally, run this command 24 | 25 | ``` 26 | $ docker-compose up 27 | ``` 28 | 29 | #### Database Migration and Seed 30 | 31 | ``` 32 | # Run database migration 33 | $ docker-compose exec api python manage.py db upgrade 34 | 35 | # Run database seed 36 | $ docker-compose exec api python manage.py seed_db 37 | ``` 38 | 39 | The seeder will contain sample users data: 40 | 41 | ``` 42 | username : admin 43 | password : verysecurepassword 44 | ``` 45 | 46 | After you run above commands you can open the application from [http://localhost:8080/](http://localhost:8080/) 47 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | db.sqlite3 4 | __pycache__ 5 | *.pyc 6 | *.pyo 7 | *.pyd 8 | .Python 9 | env 10 | pip-log.txt 11 | pip-delete-this-directory.txt 12 | .tox 13 | .coverage 14 | .coverage.* 15 | .cache 16 | coverage.xml 17 | *,cover 18 | *.log 19 | .git 20 | htmlcov/ 21 | data/ 22 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | htmlcov 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | .DS_Store 127 | 128 | .vercel -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base Image 2 | FROM python:3.7.2-slim 3 | 4 | # Install netcat 5 | RUN apt-get update && \ 6 | apt-get -y install netcat && \ 7 | apt-get clean 8 | 9 | # set working directory 10 | WORKDIR /usr/src/app 11 | 12 | # add and install requirements 13 | COPY ./requirements.txt /usr/src/app/requirements.txt 14 | RUN pip install -r requirements.txt 15 | 16 | # add app 17 | COPY . /usr/src/app 18 | EXPOSE 5000 19 | # run server 20 | CMD python index.py 21 | -------------------------------------------------------------------------------- /api/Dockerfile-prod: -------------------------------------------------------------------------------- 1 | # base image 2 | FROM python:3.7.2-slim 3 | 4 | # install netcat 5 | RUN apt-get update && \ 6 | apt-get -y install netcat && \ 7 | apt-get clean 8 | 9 | # set working directory 10 | WORKDIR /usr/src/app 11 | 12 | # add and install requirements 13 | COPY ./requirements.txt /usr/src/app/requirements.txt 14 | RUN pip install -r requirements.txt 15 | 16 | # add app 17 | COPY . /usr/src/app 18 | 19 | # run server 20 | CMD gunicorn -b 0.0.0.0:5000 manage:app 21 | 22 | -------------------------------------------------------------------------------- /api/index.py: -------------------------------------------------------------------------------- 1 | from project import create_app 2 | 3 | app = create_app() 4 | 5 | if __name__ == '__main__': 6 | app.run(debug=True, host='0.0.0.0') 7 | -------------------------------------------------------------------------------- /api/manage.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import coverage 4 | from flask.cli import FlaskGroup 5 | from project.modules.users.models import Users 6 | from project import create_app 7 | from project.extensions import db 8 | 9 | COV = coverage.coverage(branch=True, 10 | include='project/*', 11 | omit=['project/tests/*', 'project/config.py']) 12 | COV.start() 13 | 14 | app = create_app() 15 | cli = FlaskGroup(create_app=create_app) 16 | 17 | 18 | @cli.command('recreate_db') 19 | def recreate_db(): 20 | db.drop_all() 21 | db.create_all() 22 | db.session.commit() 23 | 24 | 25 | @cli.command() 26 | def test(): 27 | """Run unit test without code coverage""" 28 | tests = unittest.TestLoader().discover('project/tests', pattern='test*.py') 29 | result = unittest.TextTestRunner(verbosity=2).run(tests) 30 | if result.wasSuccessful(): 31 | return 0 32 | 33 | sys.exit(result) 34 | 35 | 36 | @cli.command('seed_db') 37 | def seed_db(): 38 | """Seeds the database.""" 39 | db.session.add( 40 | Users(first_name='admin', 41 | last_name='admin', 42 | email='admin@gmail.com', 43 | password='verysecurepassword')) 44 | db.session.commit() 45 | 46 | 47 | @cli.command() 48 | def cov(): 49 | """Runs the unit tests with coverage.""" 50 | tests = unittest.TestLoader().discover('project/tests', pattern='test*.py') 51 | result = unittest.TextTestRunner(verbosity=2).run(tests) 52 | if result.wasSuccessful(): 53 | COV.stop() 54 | COV.save() 55 | print('Coverage summary:') 56 | COV.report() 57 | COV.html_report() 58 | COV.erase() 59 | return 0 60 | sys.exit(result) 61 | 62 | 63 | if __name__ == '__main__': 64 | cli() 65 | -------------------------------------------------------------------------------- /api/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /api/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 | -------------------------------------------------------------------------------- /api/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', current_app.config.get( 27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /api/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /api/migrations/versions/94c5bdd99d14_initial_migration.py: -------------------------------------------------------------------------------- 1 | """initial_migration 2 | 3 | Revision ID: 94c5bdd99d14 4 | Revises: 5 | Create Date: 2020-04-10 13:43:33.733619 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '94c5bdd99d14' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('users', 22 | sa.Column('created', sa.DateTime(), nullable=False), 23 | sa.Column('updated', sa.DateTime(), nullable=False), 24 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 25 | sa.Column('first_name', sa.String(length=255), nullable=False), 26 | sa.Column('last_name', sa.String(length=255), nullable=False), 27 | sa.Column('email', sa.String(length=128), nullable=True), 28 | sa.Column('password', sa.String(length=255), nullable=False), 29 | sa.Column('active', sa.Boolean(), nullable=False), 30 | sa.PrimaryKeyConstraint('id'), 31 | sa.UniqueConstraint('email') 32 | ) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_table('users') 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /api/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "user-management-example-api", 4 | "builds": [ 5 | { 6 | "src": "*.py", 7 | "use": "@liudonghua123/now-flask", 8 | "config": { "maxLambdaSize": "30mb" } 9 | } 10 | ], 11 | "routes": [{ "src": "/.*", "dest": "/" }] 12 | } 13 | -------------------------------------------------------------------------------- /api/project/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Factory for main flask application 3 | """ 4 | import os 5 | 6 | from flask import Flask 7 | from flask_migrate import Migrate 8 | from werkzeug.middleware.proxy_fix import ProxyFix 9 | 10 | migrate = Migrate() 11 | 12 | 13 | def create_app(flask_config_name=None, **kwargs): 14 | """ 15 | Entry point to the Flask RESTful Server application. 16 | """ 17 | # This is a workaround for Alpine Linux (musl libc) quirk: 18 | # https://github.com/docker-library/python/issues/211 19 | import threading 20 | threading.stack_size(2 * 1024 * 1024) 21 | 22 | env_flask_config_name = os.getenv('APP_SETTINGS') 23 | 24 | app = Flask(__name__, **kwargs) 25 | app.wsgi_app = ProxyFix(app.wsgi_app) 26 | 27 | app.config.from_object(env_flask_config_name) 28 | 29 | from . import extensions 30 | extensions.init_app(app) 31 | 32 | migrate.init_app(app, extensions.db) 33 | 34 | from . import modules 35 | modules.initiate_app(app) 36 | 37 | @app.shell_context_processor 38 | def ctx(): 39 | return {'app': app, 'db': extensions.db} 40 | 41 | return app 42 | -------------------------------------------------------------------------------- /api/project/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class BaseConfig: 5 | PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) 6 | TESTING = False 7 | SECRET_KEY = os.environ.get('SECRET_KEY') 8 | SQLALCHEMY_TRACK_MODIFICATIONS = False 9 | BCRYPT_LOG_ROUNDS = 13 10 | TOKEN_EXPIRATION_DAYS = 30 11 | TOKEN_EXPIRATION_SECONDS = 0 12 | 13 | class DevelopmentConfig(BaseConfig): 14 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 15 | BCRYPT_LOG_ROUNDS = 4 16 | 17 | 18 | class TestingConfig(BaseConfig): 19 | TESTING = True 20 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_TEST_URL') 21 | BCRYPT_LOG_ROUNDS = 4 22 | TOKEN_EXPIRATION_DAYS = 0 23 | TOKEN_EXPIRATION_SECONDS = 3 24 | 25 | 26 | class ProductionConfig(BaseConfig): 27 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 28 | -------------------------------------------------------------------------------- /api/project/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:11.1-alpine 2 | 3 | ADD create.sql /docker-entrypoint-initdb.d 4 | -------------------------------------------------------------------------------- /api/project/db/create.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE prod_db; 2 | CREATE DATABASE dev_db; 3 | CREATE DATABASE test_db; 4 | -------------------------------------------------------------------------------- /api/project/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | Extensions setup 4 | ================ 5 | 6 | Extensions provide access to common resources of the application. 7 | 8 | Please, put new extension instantiations and initializations here. 9 | """ 10 | 11 | from flask_sqlalchemy import SQLAlchemy 12 | from flask_marshmallow import Marshmallow 13 | from flask_cors import CORS 14 | from flask_bcrypt import Bcrypt 15 | from .logging import Logging 16 | from logging.config import dictConfig 17 | 18 | 19 | logging = Logging() 20 | 21 | dictConfig({ 22 | 'version': 1, 23 | 'formatters': {'default': { 24 | 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', 25 | }}, 26 | 'handlers': {'wsgi': { 27 | 'class': 'logging.StreamHandler', 28 | 'formatter': 'default' 29 | }}, 30 | 'root': { 31 | 'level': 'DEBUG', 32 | 'handlers': ['wsgi'] 33 | } 34 | }) 35 | 36 | cross_origin_resource_sharing = CORS() 37 | db = SQLAlchemy() 38 | ma = Marshmallow() 39 | bcrypt = Bcrypt() 40 | 41 | 42 | def init_app(app): 43 | """ 44 | Application extensions initialization. 45 | """ 46 | for extension in ( 47 | logging, 48 | cross_origin_resource_sharing, 49 | db, 50 | ma, 51 | bcrypt 52 | ): 53 | extension.init_app(app) 54 | -------------------------------------------------------------------------------- /api/project/extensions/logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logging adapter 3 | --------------- 4 | """ 5 | import logging 6 | 7 | 8 | class Logging(object): 9 | """ 10 | This is a helper extension, which adjusts logging configuration for the 11 | application. 12 | """ 13 | 14 | def __init__(self, app=None): 15 | if app: 16 | self.init_app(app) 17 | 18 | def init_app(self, app): 19 | """ 20 | Common Flask interface to initialize the logging according to the 21 | application configuration. 22 | """ 23 | # We don't need the default Flask's loggers when using our invoke tasks 24 | # since we set up beautiful colorful loggers globally. 25 | for handler in list(app.logger.handlers): 26 | app.logger.removeHandler(handler) 27 | app.logger.propagate = True 28 | 29 | # We don't need the default SQLAlchemy loggers when using our invoke 30 | # tasks since we set up beautiful colorful loggers globally. 31 | # NOTE: This particular workaround is for the SQLALCHEMY_ECHO mode, 32 | # when all SQL commands get printed (without these lines, they will get 33 | # printed twice). 34 | sqla_logger = logging.getLogger('sqlalchemy.engine.base.Engine') 35 | for hdlr in list(sqla_logger.handlers): 36 | sqla_logger.removeHandler(hdlr) 37 | sqla_logger.addHandler(logging.NullHandler()) 38 | -------------------------------------------------------------------------------- /api/project/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from project.modules.users import api as users_api 2 | from project.modules.auth import api as auth_api 3 | from project.modules.auth.registration import api as registration_api 4 | from flask_restx import Api 5 | 6 | authorizations = { 7 | 'apikey': { 8 | 'type': 'apiKey', 9 | 'in': 'header', 10 | 'name': 'Authorization' 11 | } 12 | } 13 | api = Api(title="User Management Example API", 14 | authorizations=authorizations, 15 | version="1.0") 16 | 17 | 18 | def initiate_app(app, **kwargs): 19 | api.add_namespace(registration_api, path="/api/v1/registrations") 20 | api.add_namespace(users_api, path="/api/v1/users") 21 | api.add_namespace(auth_api, path="/api/v1/auth") 22 | api.init_app(app) 23 | -------------------------------------------------------------------------------- /api/project/modules/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify 2 | from http import HTTPStatus 3 | from sqlalchemy import exc, or_ 4 | from project.extensions import bcrypt 5 | from project.modules.utils import authenticate 6 | from project.modules.users.models import Users 7 | from flask_restx import Resource, Namespace, fields 8 | from project.modules.utils import authenticate_restful 9 | 10 | api = Namespace("Authentication", 11 | description="Authentication related operations") 12 | 13 | login_model = api.model( 14 | 'Login', { 15 | "email": fields.String(required=True, 16 | description="User email for login"), 17 | "password": fields.String(required=True, 18 | description="Password for login") 19 | }) 20 | 21 | 22 | @api.route('/login') 23 | class Login(Resource): 24 | @api.expect(login_model, validate=True) 25 | def post(self): 26 | post_data = api.payload 27 | response_object = {'status': 'fail', 'message': 'Invalid payload'} 28 | if not post_data: 29 | return response_object, HTTPStatus.BAD_REQUEST 30 | 31 | email = post_data.get('email') 32 | password = post_data.get('password') 33 | try: 34 | user = Users.query.filter_by(email=email).first() 35 | if user and bcrypt.check_password_hash(user.password, password): 36 | auth_token = user.encode_auth_token(user.id) 37 | if auth_token: 38 | response_object = { 39 | 'status': 'success', 40 | 'message': 'Successfully logged in', 41 | 'auth_token': auth_token.decode() 42 | } 43 | return response_object, HTTPStatus.OK 44 | else: 45 | response_object['message'] = 'User does not exist' 46 | return response_object, HTTPStatus.NOT_FOUND 47 | except Exception as e: 48 | api.logger.error(e) 49 | response_object['message'] = 'Try again.' 50 | return response_object, HTTPStatus.INTERNAL_SERVER_ERROR 51 | 52 | 53 | @api.route('/logout') 54 | class Logout(Resource): 55 | @authenticate_restful 56 | @api.doc(security="apikey") 57 | def get(self, user): 58 | response_object = { 59 | 'status': 'success', 60 | 'message': 'Successfully logged out' 61 | } 62 | return response_object, HTTPStatus.OK 63 | -------------------------------------------------------------------------------- /api/project/modules/auth/registration.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime, timedelta 3 | from flask import Blueprint, current_app 4 | from project.modules.users.models import Users 5 | from project.modules.utils import generate_token_from_data 6 | from http import HTTPStatus 7 | from flask_restx import Resource, Namespace, fields 8 | from flask_sqlalchemy import orm 9 | from project.extensions import db 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | api = Namespace("Registration", description="Registration related operations") 14 | 15 | registration_model = api.model( 16 | 'Registration', { 17 | "firstName": 18 | fields.String(required=True, description="New user first name"), 19 | "lastName": 20 | fields.String(required=True, description="New user last name"), 21 | "email": 22 | fields.String(required=True, description="New user email"), 23 | "password": 24 | fields.String(required=True, description="New user password") 25 | }) 26 | 27 | 28 | @api.route('') 29 | class Registrations(Resource): 30 | @api.expect(registration_model, validate=True) 31 | @api.doc( 32 | responses={ 33 | 201: "New user successfully created", 34 | 500: "There's an error while register a new user", 35 | 400: "There's a missing payload for the request" 36 | }) 37 | def post(self): 38 | post_data = api.payload 39 | response_object = {'status': 'fail', 'message': 'Invalid payload'} 40 | if not post_data: 41 | return response_object, HTTPStatus.BAD_REQUEST 42 | 43 | first_name = post_data.get('firstName') 44 | last_name = post_data.get('lastName') 45 | email = post_data.get('email') 46 | password = post_data.get('password') 47 | 48 | try: 49 | user = Users.query.filter_by(email=email).first() 50 | if not user: 51 | # add new user to db 52 | new_user = Users(first_name=first_name, 53 | last_name=last_name, 54 | email=email, 55 | password=password) 56 | db.session.add(new_user) 57 | db.session.commit() 58 | 59 | confirmation_token = self.generate_activation_token( 60 | new_user.email) 61 | return response_object, HTTPStatus.CREATED 62 | else: 63 | response_object['message'] = 'Sorry. That user already exists.' 64 | return response_object, HTTPStatus.BAD_REQUEST 65 | except Exception as e: 66 | db.session.rollback() 67 | api.logger.error(e) 68 | response_object = {"message": "Successfully"} 69 | return response_object, HTTPStatus.CREATED 70 | 71 | def generate_activation_token(self, email: str) -> str: 72 | token_max_expiration = current_app.config[ 73 | 'ACTIVATION_TOKEN_MAX_EXPIRATION'] 74 | token_expiration = datetime.now() + timedelta( 75 | hours=float(token_max_expiration)) 76 | token_data = { 77 | "email": email, 78 | "expired": token_expiration.strftime('%d/%m/%Y %H:%M:%S') 79 | } 80 | confirmation_token = generate_token_from_data(token_data) 81 | return confirmation_token 82 | -------------------------------------------------------------------------------- /api/project/modules/users/__init__.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | from flask_restx import Resource, Namespace, fields 3 | from .models import Users as UsersModel 4 | from project.modules.utils import authenticate_restful, is_admin 5 | from project.extensions import db 6 | 7 | api = Namespace("Users", description="Users related operation") 8 | 9 | 10 | @api.route('/') 11 | class Users(Resource): 12 | @authenticate_restful 13 | def get(self, user, id: int): 14 | """Get single user details""" 15 | response_object = {'status': 'fail', 'message': 'User does not exist'} 16 | try: 17 | user = UsersModel.query.filter_by(id=id).first() 18 | if not user: 19 | return response_object, HTTPStatus.NOT_FOUND 20 | else: 21 | response_object = {'status': 'success', 'data': user.to_json()} 22 | return response_object, HTTPStatus.OK 23 | except ValueError: 24 | return response_object, HTTPStatus.NOT_FOUND 25 | except Exception as e: 26 | response_object[ 27 | 'message'] = "There's an error while fetch user detail" 28 | api.logger.error(e) 29 | return response_object, HTTPStatus.INTERNAL_SERVER_ERROR 30 | 31 | 32 | @api.route('') 33 | class UsersList(Resource): 34 | @authenticate_restful 35 | @api.doc(security="apikey") 36 | def get(self, current_user): 37 | """Get all users""" 38 | response_object = { 39 | 'status': 'success', 40 | 'users': [user.to_json() for user in UsersModel.query.all()] 41 | } 42 | return response_object, HTTPStatus.OK 43 | -------------------------------------------------------------------------------- /api/project/modules/users/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import jwt 3 | import logging 4 | from project.extensions import db, bcrypt 5 | from sqlalchemy_utils import Timestamp 6 | from flask import current_app 7 | from sqlalchemy import Column 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class Users(db.Model, Timestamp): 13 | 14 | __tablename__ = "users" 15 | 16 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 17 | first_name = db.Column(db.String(255), nullable=False) 18 | last_name = db.Column(db.String(255), nullable=False) 19 | email = db.Column(db.String(128), unique=True) 20 | password = db.Column(db.String(255), nullable=False) 21 | active = db.Column(db.Boolean, default=True, nullable=False) 22 | 23 | def __init__(self, 24 | first_name: str, 25 | last_name: str, 26 | password: str, 27 | email: str = ''): 28 | self.first_name = first_name 29 | self.last_name = last_name 30 | self.email = email 31 | self.password = bcrypt.generate_password_hash( 32 | password, current_app.config.get('BCRYPT_LOG_ROUNDS')).decode() 33 | 34 | def to_json(self): 35 | return { 36 | 'id': self.id, 37 | 'firstName': self.first_name, 38 | 'lastName': self.last_name, 39 | 'email': self.email, 40 | 'active': self.active, 41 | } 42 | 43 | def encode_auth_token(self, user_id: int): 44 | """Generates the auth token""" 45 | try: 46 | exp_days = current_app.config.get('TOKEN_EXPIRATION_DAYS') 47 | exp_sec = current_app.config.get('TOKEN_EXPIRATION_SECONDS') 48 | payload = { 49 | 'exp': 50 | datetime.datetime.utcnow() + 51 | datetime.timedelta(days=exp_days, seconds=exp_sec), 52 | 'iat': 53 | datetime.datetime.utcnow(), 54 | 'id': 55 | user_id 56 | } 57 | return jwt.encode(payload, 58 | current_app.config.get('SECRET_KEY'), 59 | algorithm='HS256') 60 | except Exception as e: 61 | return e 62 | 63 | @staticmethod 64 | def decode_auth_token(auth_token: bytes): 65 | """ 66 | Decodes the auth token 67 | - :param auth_token: 68 | - :return integer|string 69 | """ 70 | try: 71 | payload = jwt.decode(auth_token, 72 | current_app.config.get('SECRET_KEY')) 73 | return payload['id'] 74 | except jwt.ExpiredSignatureError: 75 | return 'Signature expired. Please log in again.' 76 | except jwt.InvalidTokenError: 77 | return 'Invalid token. Please log in again.' 78 | -------------------------------------------------------------------------------- /api/project/modules/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from http import HTTPStatus 3 | from functools import wraps 4 | from flask import request, jsonify 5 | from project.modules.users.models import Users 6 | from itsdangerous.url_safe import URLSafeSerializer 7 | 8 | 9 | def is_admin(user_id: int) -> bool: 10 | user = Users.query.filter_by(id=user_id).first() 11 | return user.admin 12 | 13 | 14 | def generate_token_from_data(data: dict, salt: str = "user_activation") -> str: 15 | secret_key = os.environ.get('SECRET_KEY') 16 | serializer = URLSafeSerializer(secret_key) 17 | token = serializer.dumps(data, salt=salt) 18 | return token 19 | 20 | 21 | def authenticate(f): 22 | @wraps(f) 23 | def decorated_function(*args, **kwargs): 24 | response_object = { 25 | 'status': 'fail', 26 | 'message': 'Provide a valid auth token.' 27 | } 28 | auth_header = request.headers.get('Authorization') 29 | print(auth_header) 30 | if not auth_header: 31 | return jsonify(response_object), HTTPStatus.FORBIDDEN 32 | auth_token = auth_header.split(" ")[1] 33 | resp = Users.decode_auth_token(auth_token) 34 | if isinstance(resp, str): 35 | response_object['message'] = resp 36 | return jsonify(response_object), HTTPStatus.UNAUTHORIZED 37 | 38 | user = Users.query.filter_by(id=resp).first() 39 | if not user or not user.active: 40 | return jsonify(response_object), HTTPStatus.UNAUTHORIZED 41 | return f(user, *args, **kwargs) 42 | 43 | return decorated_function 44 | 45 | 46 | def authenticate_restful(f): 47 | @wraps(f) 48 | def decorated_function(*args, **kwargs): 49 | response_object = { 50 | 'status': 'fail', 51 | 'message': 'Provide a valid auth token.' 52 | } 53 | auth_header = request.headers.get('Authorization') 54 | if not auth_header: 55 | return response_object, HTTPStatus.FORBIDDEN 56 | auth_token = auth_header.split(" ")[1] 57 | resp = Users.decode_auth_token(auth_token) 58 | if isinstance(resp, str): 59 | response_object['message'] = resp 60 | return response_object, HTTPStatus.UNAUTHORIZED 61 | user = Users.query.filter_by(id=resp).first() 62 | if not user or not user.active: 63 | return response_object, HTTPStatus.UNAUTHORIZED 64 | return f(user, *args, **kwargs) 65 | 66 | return decorated_function 67 | -------------------------------------------------------------------------------- /api/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | docker-compose==1.25.4 4 | jedi==0.16.0 5 | yapf==0.29.0 6 | pylint==2.4.4 7 | pylama==7.7.1 8 | pip-review==1.1.0 9 | -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==5.0.4 2 | Flask==1.1.2 3 | Flask-SQLAlchemy==2.4.1 4 | psycopg2-binary==2.8.5 5 | itsdangerous==1.1.0 6 | gunicorn==20.0.4 7 | flask-bcrypt==0.7.1 8 | flask-cors==3.0.8 9 | flask-marshmallow==0.11.0 10 | flask-migrate==2.5.3 11 | flask-restx==0.2.0 12 | Flask-Testing==0.8.0 13 | marshmallow-sqlalchemy==0.22.3 14 | pyjwt==1.7.1 15 | requests==2.23.0 16 | SQLAlchemy-Utils==0.36.3 17 | -------------------------------------------------------------------------------- /api/setup.cfg: -------------------------------------------------------------------------------- 1 | [pylama:pycodestyle] 2 | max_line_length = 120 3 | 4 | [pylama:pylint] 5 | max_line_length = 120 6 | -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | node_modules/ 3 | Dockerfile* 4 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | 111 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.13.0-alpine 2 | 3 | WORKDIR usr/src/app 4 | ENV PATH /usr/src/app/node_modules/.bin:$PATH 5 | 6 | # install and cache app dependencies 7 | COPY package.json /usr/src/app/package.json 8 | 9 | RUN yarn 10 | 11 | EXPOSE 3000 12 | CMD ["yarn", "dev"] 13 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # With Docker 2 | 3 | ## How to use 4 | 5 | ### Using `create-next-app` 6 | 7 | Execute [`create-next-app`](https://github.com/segmentio/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example: 8 | 9 | ```bash 10 | npx create-next-app --example with-docker with-docker-app 11 | # or 12 | yarn create next-app --example with-docker with-docker-app 13 | ``` 14 | 15 | ### Download manually 16 | 17 | Download the example: 18 | 19 | ```bash 20 | curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-docker 21 | cd with-docker 22 | ``` 23 | 24 | Build it with docker: 25 | 26 | ```bash 27 | # build 28 | docker build -t next-app . 29 | # or, use multi-stage builds to build a smaller docker image 30 | docker build -t next-app -f ./Dockerfile.multistage . 31 | ``` 32 | 33 | Run it: 34 | 35 | ```bash 36 | docker run --rm -it \ 37 | -p 3000:3000 \ 38 | -e "API_URL=https://example.com" \ 39 | next-app 40 | ``` 41 | 42 | Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)) 43 | 44 | ```bash 45 | now --docker -e API_URL="https://example.com" 46 | ``` 47 | 48 | ## The idea behind the example 49 | 50 | This example show how to set custom environment variables for your **docker application** at runtime. 51 | 52 | The `dockerfile` is the simplest way to run Next.js app in docker, and the size of output image is `173MB`. However, for an even smaller build, you can do multi-stage builds with `dockerfile.multistage`. The size of output image is `85MB`. 53 | 54 | You can check the [Example Dockerfile for your own Node.js project](https://github.com/mhart/alpine-node/tree/43ca9e4bc97af3b1f124d27a2cee002d5f7d1b32#example-dockerfile-for-your-own-nodejs-project) section in [mhart/alpine-node](https://github.com/mhart/alpine-node) for more details. 55 | -------------------------------------------------------------------------------- /client/components/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | export class ErrorBoundary extends Component { 4 | state = { 5 | error: null, 6 | eventId: null 7 | }; 8 | 9 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 10 | this.setState({ error }); 11 | console.log(error, errorInfo); 12 | } 13 | 14 | render() { 15 | if (this.state.error) { 16 | return
Error
; 17 | } else { 18 | return this.props.children; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/components/errors/five_hundred_error.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Link from "next/link"; 4 | 5 | const Container = styled.div` 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | height: 100vh; 10 | overflow: hidden; 11 | flex-direction: column; 12 | `; 13 | 14 | export const FiveHundredError = () => { 15 | return ( 16 | 17 |
18 | 195 |
196 |
197 |
198 | 199 | Go back to Home 200 | 201 |
202 |
203 | ); 204 | }; 205 | -------------------------------------------------------------------------------- /client/components/errors/four_zero_four_error.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Link from "next/link"; 4 | 5 | const Container = styled.div` 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | height: 100vh; 10 | overflow: hidden; 11 | flex-direction: column; 12 | 13 | .error-code { 14 | &:after { 15 | content: "Looks like you are lost!" !important; 16 | } 17 | } 18 | `; 19 | 20 | export const FourZeroFour = () => { 21 | return ( 22 | 23 |
24 | 189 |
190 |
191 |
192 | The page you are looking for might have been removed had its name 193 | changed or is temporarily unavailable. 194 |
195 | 196 | Back to homepage 197 | 198 |
199 |
200 | ); 201 | }; 202 | -------------------------------------------------------------------------------- /client/components/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./five_hundred_error"; 2 | export * from "./four_zero_four_error"; 3 | -------------------------------------------------------------------------------- /client/components/layout/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const CustomFooter = styled.footer` 5 | position: absolute; 6 | bottom: 0; 7 | width: 100%; 8 | line-height: 60px; 9 | background-color: #f5f5f5; 10 | `; 11 | 12 | export const Footer = () => { 13 | return ( 14 | 15 |
16 | 17 | © 18 | {`${new Date().getFullYear()} Company Inc. All Rights Reserved`} 19 | 20 |
21 | 26 | Github 27 | 28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | Footer.displayName = "Footer"; 35 | -------------------------------------------------------------------------------- /client/components/layout/header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Collapse, Navbar, NavbarToggler, NavbarBrand } from "reactstrap"; 3 | import { Menu } from "./menu"; 4 | 5 | export const Header = () => { 6 | const [isOpen, setIsOpen] = useState(false); 7 | 8 | const toggle = () => { 9 | setIsOpen(!isOpen); 10 | }; 11 | return ( 12 |
13 |
14 | 15 | Flask/NextJs App 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /client/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./header"; 2 | export * from "./footer"; 3 | -------------------------------------------------------------------------------- /client/components/layout/menu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Nav, 4 | NavItem, 5 | NavLink, 6 | UncontrolledDropdown, 7 | DropdownToggle, 8 | DropdownMenu, 9 | DropdownItem, 10 | } from "reactstrap"; 11 | import Link from "next/link"; 12 | import { logout, useAuth } from "utils/auth"; 13 | import Router from "next/router"; 14 | 15 | export const Menu = () => { 16 | const auth = useAuth(); 17 | 18 | const onLogoutClick = () => { 19 | logout(); 20 | Router.push("/login"); 21 | }; 22 | 23 | return ( 24 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /client/components/text-input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type TextInputProps = { 4 | id: string; 5 | name: string; 6 | label: string; 7 | required: boolean; 8 | value: string; 9 | type?: "email" | "text" | "password"; 10 | onChange: (e: React.ChangeEvent) => void; 11 | }; 12 | 13 | export const TextInput = (props: TextInputProps) => { 14 | return ( 15 |
16 | 17 | 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | const withCss = require("@zeit/next-css"); 2 | const withPlugins = require("next-compose-plugins"); 3 | const TsConfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); 4 | 5 | const nextConfig = { 6 | webpack: config => { 7 | if (config.resolve.plugins) { 8 | config.resolve.plugins.push(new TsConfigPathsPlugin()); 9 | } else { 10 | config.resolve.plugins = [new TsConfigPathsPlugin()]; 11 | } 12 | 13 | return config; 14 | }, 15 | serverRuntimeConfig: { 16 | // Will only be available on the server side 17 | mySecret: "secret" 18 | }, 19 | env: { 20 | // Will be available on both server and client 21 | API_URL: process.env.REACT_APP_SERVICE_URL 22 | } 23 | }; 24 | 25 | // next.config.js 26 | module.exports = withPlugins([withCss], nextConfig); 27 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flask-react-docker-app", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build", 7 | "start": "next start" 8 | }, 9 | "dependencies": { 10 | "@zeit/next-css": "1.0.1", 11 | "apisauce": "~1.1.1", 12 | "isomorphic-unfetch": "~3.0.0", 13 | "js-cookie": "~2.2.1", 14 | "jwt-decode": "~2.2.0", 15 | "moment": "~2.26.0", 16 | "next": "~9.4.4", 17 | "next-cookies": "~2.0.3", 18 | "nookies": "~2.3.1", 19 | "nprogress": "~0.2.0", 20 | "react": "~16.13.1", 21 | "react-dom": "~16.13.1", 22 | "reactstrap": "~8.4.1", 23 | "styled-components": "~5.1.1" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "~14.0.13", 27 | "@types/react": "~16.9.36", 28 | "@types/react-dom": "~16.9.6", 29 | "@types/styled-components": "~5.1.0", 30 | "babel-plugin-styled-components": "~1.10.7", 31 | "cross-env": "~7.0.2", 32 | "next-compose-plugins": "~2.2.0", 33 | "tsconfig-paths-webpack-plugin": "~3.2.0", 34 | "typescript": "~3.9.5" 35 | }, 36 | "license": "ISC" 37 | } 38 | -------------------------------------------------------------------------------- /client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import App from "next/app"; 2 | import Router from "next/router"; 3 | import NProgress from "nprogress"; 4 | import { Header, Footer } from "components/layout"; 5 | import { ErrorBoundary } from "components/error-boundary"; 6 | import { ProvideAuth, AuthToken, TOKEN_STORAGE_NAME } from "utils/auth"; 7 | import { Container } from "reactstrap"; 8 | import nookies from "nookies"; 9 | 10 | type CustomAppProps = { 11 | auth?: AuthToken; 12 | }; 13 | 14 | export default class CustomApp extends App { 15 | static async getInitialProps({ Component, ctx }) { 16 | const token = nookies.get(ctx)[TOKEN_STORAGE_NAME]; 17 | let initialProps = {}; 18 | if (token) { 19 | const auth = new AuthToken(token); 20 | initialProps = { auth }; 21 | } 22 | if (Component.getInitialProps) { 23 | return Component.getInitialProps(ctx, initialProps); 24 | } 25 | 26 | return initialProps; 27 | } 28 | 29 | get auth() { 30 | if (this.props.auth) { 31 | // the server pass to the client serializes the token 32 | // so we have to reinitialize the authToken class 33 | // 34 | // @see https://github.com/zeit/next.js/issues/3536 35 | return new AuthToken(this.props.auth.token); 36 | } 37 | return null; 38 | } 39 | 40 | componentDidMount() { 41 | Router.events.on("routeChangeComplete", () => { 42 | NProgress.start(); 43 | }); 44 | 45 | Router.events.on("routeChangeComplete", () => NProgress.done()); 46 | Router.events.on("routeChangeError", () => NProgress.done()); 47 | } 48 | 49 | componentDidCatch(error: any, errorInfo: any) { 50 | super.componentDidCatch(error, errorInfo); 51 | } 52 | 53 | render() { 54 | const { Component, pageProps } = this.props; 55 | 56 | return ( 57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 |