├── .flaskenv ├── dev-requirements.txt ├── migrations ├── README ├── script.py.mako ├── alembic.ini ├── versions │ └── 20201120_150602_create_users_table.py └── env.py ├── app ├── models │ ├── __init__.py │ ├── db.py │ └── user.py ├── forms │ ├── __init__.py │ ├── signup_form.py │ └── login_form.py ├── config.py ├── api │ ├── user_routes.py │ └── auth_routes.py ├── seeds │ ├── __init__.py │ └── users.py └── __init__.py ├── .gitignore ├── react-app ├── src │ ├── index.css │ ├── components │ │ ├── NavBar │ │ │ ├── index.js │ │ │ └── NavBar.js │ │ ├── LoginForm │ │ │ ├── index.js │ │ │ └── LoginForm.js │ │ ├── SignUpForm │ │ │ ├── index.js │ │ │ └── SignUpForm.js │ │ ├── LogoutButton │ │ │ ├── index.js │ │ │ └── LogoutButton.js │ │ └── ProtectedRoute │ │ │ ├── index.js │ │ │ └── ProtectedRoute.js │ ├── context │ │ ├── Modal.css │ │ └── Modal.js │ ├── index.js │ ├── store │ │ ├── index.js │ │ └── session.js │ └── App.js ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── craco.config.js ├── tailwind.config.js ├── .gitignore ├── package.json └── README.md ├── .dockerignore ├── .vim └── coc-settings.json ├── .env.example ├── .vscode └── settings.json ├── Dockerfile ├── Pipfile ├── requirements.txt ├── README.md └── Pipfile.lock /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary==2.8.6 2 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ 3 | *.py[cod] 4 | .venv 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .login_form import LoginForm 2 | from .signup_form import SignUpForm -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | react-app/node_modules 2 | .venv 3 | Pipfile 4 | Pipfile.lock 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import NavBar from './NavBar'; 2 | 3 | export default NavBar; 4 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/petermai/Documents/FlaskReact/.venv/bin/python" 3 | } -------------------------------------------------------------------------------- /react-app/src/components/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | import LoginForm from './LoginForm'; 2 | 3 | export default LoginForm; 4 | -------------------------------------------------------------------------------- /react-app/src/components/SignUpForm/index.js: -------------------------------------------------------------------------------- 1 | import SignUpForm from './SignUpForm'; 2 | 3 | export default SignUpForm; 4 | -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/flask-react-template/HEAD/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/src/components/LogoutButton/index.js: -------------------------------------------------------------------------------- 1 | import LogoutButton from './LogoutButton'; 2 | 3 | export default LogoutButton; 4 | -------------------------------------------------------------------------------- /react-app/src/components/ProtectedRoute/index.js: -------------------------------------------------------------------------------- 1 | import ProtectedRoute from './ProtectedRoute'; 2 | 3 | export default ProtectedRoute; 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230 4 | DATABASE_URL=postgresql://starter_app_dev:password@localhost/starter_app 5 | -------------------------------------------------------------------------------- /react-app/craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | postcss: { 4 | plugins: [ 5 | require('tailwindcss'), 6 | require('autoprefixer'), 7 | ], 8 | }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config: 5 | SECRET_KEY = os.environ.get('SECRET_KEY') 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 8 | SQLALCHEMY_ECHO = True 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/home/echo/dev/appacademy/starters/python-project-starter/.venv/bin/python", 3 | "python.linting.pylintEnabled": false, 4 | "python.linting.enabled": true, 5 | "python.linting.pycodestyleEnabled": true 6 | } -------------------------------------------------------------------------------- /react-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /react-app/src/components/ProtectedRoute/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | 4 | const ProtectedRoute = props => { 5 | return ( 6 | 7 | {(props.authenticated)? props.children : } 8 | 9 | ); 10 | }; 11 | 12 | 13 | export default ProtectedRoute; 14 | -------------------------------------------------------------------------------- /react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /react-app/src/context/Modal.css: -------------------------------------------------------------------------------- 1 | #modal { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | left: 0; 6 | bottom: 0; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | #modal-background { 13 | position: fixed; 14 | top: 0; 15 | right: 0; 16 | left: 0; 17 | bottom: 0; 18 | background-color: rgba(0, 0, 0, 0.7); 19 | } 20 | 21 | #modal-content { 22 | position: absolute; 23 | background-color:white; 24 | border-radius: 0.5rem; 25 | } 26 | -------------------------------------------------------------------------------- /app/api/user_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from flask_login import login_required 3 | from app.models import User 4 | 5 | user_routes = Blueprint('users', __name__) 6 | 7 | 8 | @user_routes.route('/') 9 | @login_required 10 | def users(): 11 | users = User.query.all() 12 | return {"users": [user.to_dict() for user in users]} 13 | 14 | 15 | @user_routes.route('/') 16 | @login_required 17 | def user(id): 18 | user = User.query.get(id) 19 | return user.to_dict() 20 | -------------------------------------------------------------------------------- /react-app/src/components/LogoutButton/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { logout } from '../../store/session'; 5 | 6 | const LogoutButton = ({setAuthenticated}) => { 7 | const dispatch = useDispatch(); 8 | 9 | const onLogout = async (e) => { 10 | await dispatch(logout()); 11 | setAuthenticated(false); 12 | }; 13 | 14 | return ; 15 | }; 16 | 17 | export default LogoutButton; 18 | -------------------------------------------------------------------------------- /app/seeds/__init__.py: -------------------------------------------------------------------------------- 1 | from flask.cli import AppGroup 2 | from .users import seed_users, undo_users 3 | 4 | # Creates a seed group to hold our commands 5 | # So we can type `flask seed --help` 6 | seed_commands = AppGroup('seed') 7 | 8 | # Creates the `flask seed all` command 9 | @seed_commands.command('all') 10 | def seed(): 11 | seed_users() 12 | # Add other seed functions here 13 | 14 | # Creates the `flask seed undo` command 15 | @seed_commands.command('undo') 16 | def undo(): 17 | undo_users() 18 | # Add other undo functions here 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Flask React Template App", 3 | "name": "Flask React Template App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /app/seeds/users.py: -------------------------------------------------------------------------------- 1 | from werkzeug.security import generate_password_hash 2 | from app.models import db, User 3 | 4 | # Adds a demo user, you can add other users here if you want 5 | def seed_users(): 6 | 7 | demo = User(username='Demo', email='demo@aa.io', 8 | password='password') 9 | 10 | db.session.add(demo) 11 | 12 | db.session.commit() 13 | 14 | # Uses a raw SQL query to TRUNCATE the users table. 15 | # SQLAlchemy doesn't have a built in function to do this 16 | # TRUNCATE Removes all the data from the table, and resets 17 | # the auto incrementing primary key 18 | def undo_users(): 19 | db.session.execute('TRUNCATE users;') 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | import './index.css'; 7 | import App from './App'; 8 | import configureStore from './store'; 9 | 10 | const store = configureStore(); 11 | 12 | if (process.env.NODE_ENV !== 'production') { 13 | window.store = store; 14 | } 15 | 16 | const Root = () => ( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | ReactDOM.render( 25 | 26 | 27 | , 28 | document.getElementById('root') 29 | ); 30 | -------------------------------------------------------------------------------- /app/forms/signup_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | from app.models import User 5 | 6 | 7 | def user_exists(form, field): 8 | print("Checking if user exits", field.data) 9 | email = field.data 10 | user = User.query.filter(User.email == email).first() 11 | if user: 12 | raise ValidationError("User is already registered.") 13 | 14 | 15 | class SignUpForm(FlaskForm): 16 | username = StringField('username', validators=[DataRequired()]) 17 | email = StringField('email', validators=[DataRequired(), user_exists]) 18 | password = StringField('password', validators=[DataRequired()]) 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 AS build-stage 2 | 3 | WORKDIR /react-app 4 | COPY react-app/. . 5 | 6 | # You have to set this because it should be set during build time. 7 | ENV REACT_APP_BASE_URL= 8 | 9 | # Build our React App 10 | RUN npm install 11 | RUN npm run build 12 | 13 | FROM python:3.8 14 | 15 | # Setup Flask environment 16 | ENV FLASK_APP=app 17 | ENV FLASK_ENV=production 18 | ENV SQLALCHEMY_ECHO=True 19 | 20 | EXPOSE 8000 21 | 22 | WORKDIR /var/www 23 | COPY . . 24 | COPY --from=build-stage /react-app/build/* app/static/ 25 | 26 | # Install Python Dependencies 27 | RUN pip install -r requirements.txt 28 | RUN pip install psycopg2 29 | 30 | # Run flask environment 31 | CMD gunicorn app:app 32 | -------------------------------------------------------------------------------- /react-app/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import sessionReducer from './session'; 5 | 6 | const rootReducer = combineReducers({ 7 | session: sessionReducer, 8 | }); 9 | 10 | let enhancer; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | enhancer = applyMiddleware(thunk); 14 | } else { 15 | const logger = require('redux-logger').default; 16 | const composeEnhancers = 17 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 18 | enhancer = composeEnhancers(applyMiddleware(thunk, logger)); 19 | } 20 | 21 | const configureStore = (preloadedState) => { 22 | return createStore(rootReducer, preloadedState, enhancer); 23 | }; 24 | 25 | export default configureStore; 26 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | click = "==7.1.2" 8 | gunicorn = "==20.0.4" 9 | itsdangerous = "==1.1.0" 10 | python-dotenv = "==0.14.0" 11 | six = "==1.15.0" 12 | Flask = "==1.1.2" 13 | Flask-Cors = "==3.0.8" 14 | Flask-SQLAlchemy = "==2.4.4" 15 | Flask-WTF = "==0.14.3" 16 | Jinja2 = "==2.11.2" 17 | MarkupSafe = "==1.1.1" 18 | SQLAlchemy = "==1.3.19" 19 | Werkzeug = "==1.0.1" 20 | WTForms = "==2.3.3" 21 | Flask-JWT-Extended = "==3.24.1" 22 | email-validator = "*" 23 | Flask-Migrate = "==2.5.3" 24 | Flask-Login = "==0.5.0" 25 | alembic = "==1.4.3" 26 | python-dateutil = "==2.8.1" 27 | python-editor = "==1.0.4" 28 | Mako = "==1.1.3" 29 | PyJWT = "==1.7.1" 30 | 31 | [dev-packages] 32 | psycopg2-binary = "==2.8.6" 33 | autopep8 = "*" 34 | pylint = "*" 35 | 36 | [requires] 37 | python_version = "3.8" 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv lock --requirements 6 | # 7 | 8 | -i https://pypi.org/simple 9 | alembic==1.4.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 10 | click==7.1.2 11 | flask-cors==3.0.8 12 | flask-jwt-extended==3.24.1 13 | flask-migrate==2.5.3 14 | flask-login==0.5.0 15 | flask-sqlalchemy==2.4.4 16 | flask-wtf==0.14.3 17 | flask==1.1.2 18 | gunicorn==20.0.4 19 | itsdangerous==1.1.0 20 | jinja2==2.11.2 21 | mako==1.1.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 22 | markupsafe==1.1.1 23 | pyjwt==1.7.1 24 | python-dateutil==2.8.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 25 | python-dotenv==0.14.0 26 | python-editor==1.0.4 27 | six==1.15.0 28 | sqlalchemy==1.3.19 29 | werkzeug==1.0.1 30 | wtforms==2.3.3 31 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from werkzeug.security import generate_password_hash, check_password_hash 3 | from flask_login import UserMixin 4 | 5 | class User(db.Model, UserMixin): 6 | __tablename__ = 'users' 7 | 8 | id = db.Column(db.Integer, primary_key = True) 9 | username = db.Column(db.String(40), nullable = False, unique = True) 10 | email = db.Column(db.String(255), nullable = False, unique = True) 11 | hashed_password = db.Column(db.String(255), nullable = False) 12 | 13 | 14 | @property 15 | def password(self): 16 | return self.hashed_password 17 | 18 | 19 | @password.setter 20 | def password(self, password): 21 | self.hashed_password = generate_password_hash(password) 22 | 23 | 24 | def check_password(self, password): 25 | return check_password_hash(self.password, password) 26 | 27 | 28 | def to_dict(self): 29 | return { 30 | "id": self.id, 31 | "username": self.username, 32 | "email": self.email 33 | } 34 | -------------------------------------------------------------------------------- /react-app/src/context/Modal.js: -------------------------------------------------------------------------------- 1 | import { createContext, useRef, useState, useEffect, useContext } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './Modal.css'; 4 | 5 | const ModalContext = createContext(); 6 | 7 | export const ModalProvider = ({ children }) => { 8 | const modalRef = useRef(); 9 | const [value, setValue] = useState(); 10 | 11 | useEffect(() => { 12 | setValue(modalRef.current); 13 | }, []); 14 | 15 | return ( 16 | <> 17 | 18 | {children} 19 | 20 |
21 | 22 | ); 23 | }; 24 | 25 | export const Modal = ({ onClose, children }) => { 26 | const modalNode = useContext(ModalContext); 27 | if (!modalNode) return null; 28 | 29 | return ReactDOM.createPortal( 30 |