From 52a286ad84dd360b783e5518d66b2548f37f5dba Mon Sep 17 00:00:00 2001 From: Ryan Allen Date: Fri, 15 Oct 2021 12:54:46 -0400 Subject: [PATCH] clean up initial environment --- README.md | 69 ++-- cookiecutter.json | 20 +- hooks/post_gen_project.py | 6 + {{cookiecutter.project_slug}}/.env | 16 + {{cookiecutter.project_slug}}/.gitattributes | 4 + .../.project-gitignore | 330 ++++++++++++++++++ {{cookiecutter.project_slug}}/README.md | 9 +- .../backend/app/core/config.py | 7 +- .../backend/app/core/security.py | 7 +- .../backend/app/db/crud.py | 4 +- .../backend/app/db/session.py | 4 +- .../backend/app/initial_data.py | 7 +- .../backend/app/main.py | 3 +- .../backend/conftest.py | 2 +- .../docker-compose.yml | 12 +- .../frontend/src/config/index.tsx | 4 +- .../frontend/src/views/Home.tsx | 2 +- .../frontend/src/views/Login.tsx | 2 +- .../frontend/src/views/SignUp.tsx | 2 +- 19 files changed, 439 insertions(+), 71 deletions(-) create mode 100644 hooks/post_gen_project.py create mode 100644 {{cookiecutter.project_slug}}/.env create mode 100644 {{cookiecutter.project_slug}}/.gitattributes create mode 100644 {{cookiecutter.project_slug}}/.project-gitignore diff --git a/README.md b/README.md index f31b5e73..2217cdbd 100644 --- a/README.md +++ b/README.md @@ -48,31 +48,31 @@ modern stack. ## Table of Contents -- [Background](#background) -- [Quick Start](#quick-start) -- [Develop](#develop) -- [Admin Dashboard](#admin-dashboard) -- [Security](#security) -- [Testing](#testing) - - [Fixtures](#fixtures) - - [test_db](#test_db) - - [test_user](#test_user) - - [test_superuser](#test_superuser) - - [client](#client) - - [user_token_headers](#user_token_headers) - - [superuser_token_headers](#superuser_token_headers) -- [Background Tasks](#background-tasks) - - [Flower](#flower) -- [Frontend Utilities](#frontend-utilities) - - [Utility Functions](#utility-functions) - - [login](#login) - - [logout](#logout) - - [isAuthenticated](#isauthenticated) - - [Routes](#routes) - - [Higher Order Components](#higher-order-components) - - [PrivateRoute](#privateroute) -- [Deployment](#deployment) -- [Contributing](#contributing) + - [Background](#background) + - [Quick Start](#quick-start) + - [Develop](#develop) + - [Admin Dashboard](#admin-dashboard) + - [Security](#security) + - [Testing](#testing) + - [Fixtures](#fixtures) + - [test_db](#test_db) + - [test_user](#test_user) + - [test_superuser](#test_superuser) + - [client](#client) + - [user_token_headers](#user_token_headers) + - [superuser_token_headers](#superuser_token_headers) + - [Background Tasks](#background-tasks) + - [Flower](#flower) + - [Frontend Utilities](#frontend-utilities) + - [Utility Functions](#utility-functions) + - [login](#login) + - [logout](#logout) + - [isAuthenticated](#isauthenticated) + - [Routes](#routes) + - [Higher Order Components](#higher-order-components) + - [PrivateRoute](#privateroute) + - [Deployment](#deployment) + - [Contributing](#contributing) ## Background @@ -113,15 +113,16 @@ You will need to put in a few variables and it will create a project directory
Input Variables -- project_name [default fastapi-react-project] -- project_slug [default fastapi-react-project] - this is your project directory -- port [default 8000] -- postgres_user [default postgres] -- postgres_password [default password] -- postgres_database [default app] -- superuser_email [default admin@fastapi-react-project.com] -- superuser_password [default password] -- secret_key [default super_secret] +- project_name [default "FastAPI React Project"] +- project_slug [default "fastapi-react-project"] - this is your project directory +- project_namespace [default "fastapi_react_project"] +- project_port [default 8000] +- postgres_database [default "fastapi_react_project_db"] +- postgres_user [default "postgres"] +- postgres_password [default random_ascii_string(32)] +- superuser_email [default "admin@example.com"] +- superuser_password [default random_ascii_string(32)] +- secret_key [default random_ascii_string(128, punctuation=True)]
diff --git a/cookiecutter.json b/cookiecutter.json index d7b3507d..d2252a26 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -1,11 +1,15 @@ { - "project_slug": "fastapi-react-project", - "project_name": "{{ cookiecutter.project_slug }}", - "port": "8000", + "project_name": "FastAPI React Project", + "project_slug": "{{ cookiecutter.project_name | slugify }}", + "project_namespace": "{{ cookiecutter.project_slug | replace('-', '_') }}", + "project_port": 8000, + + "secret_key": "{{ random_ascii_string(128) }}", + + "postgres_database": "{{ cookiecutter.project_namespace }}_db", "postgres_user": "postgres", - "postgres_password": "password", - "postgres_database": "app", - "superuser_email": "admin@{{cookiecutter.project_name}}.com", - "superuser_password": "password", - "secret_key": "super_secret" + "postgres_password": "{{ random_ascii_string(32) }}", + + "superuser_email": "admin@example.com", + "superuser_password": "{{ random_ascii_string(32) }}" } diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py new file mode 100644 index 00000000..d5abbfed --- /dev/null +++ b/hooks/post_gen_project.py @@ -0,0 +1,6 @@ +import subprocess + +subprocess.call(['mv', '.project-gitignore', '.gitignore']) +subprocess.call(['git', 'init']) +subprocess.call(['git', 'add', '--all']) +subprocess.call(['git', 'commit', '-m', 'init project with cookiecutter']) diff --git a/{{cookiecutter.project_slug}}/.env b/{{cookiecutter.project_slug}}/.env new file mode 100644 index 00000000..83739705 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.env @@ -0,0 +1,16 @@ +PROJECT_NAME={{cookiecutter.project_name}} +PROJECT_SLUG={{cookiecutter.project_slug}} +PROJECT_NAMESPACE={{cookiecutter.project_namespace}} +PROJECT_PORT={{cookiecutter.project_port}} + +SECRET_KEY={{cookiecutter.secret_key}} +DEBUG=True + +POSTGRES_DB={{cookiecutter.postgres_database}} +POSTGRES_USER={{cookiecutter.postgres_user}} +POSTGRES_PASSWORD={{cookiecutter.postgres_password}} + +DATABASE_URL=postgresql://{{cookiecutter.postgres_user}}:{{cookiecutter.postgres_password}}@postgres:5432/{{cookiecutter.postgres_user}} + +SUPERUSER_EMAIL={{cookiecutter.superuser_email}} +SUPERUSER_PASSWORD={{cookiecutter.superuser_password}} diff --git a/{{cookiecutter.project_slug}}/.gitattributes b/{{cookiecutter.project_slug}}/.gitattributes new file mode 100644 index 00000000..a249ffb5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.gitattributes @@ -0,0 +1,4 @@ +* text=auto + +*.conf text eol=lf +*.sh text eol=lf diff --git a/{{cookiecutter.project_slug}}/.project-gitignore b/{{cookiecutter.project_slug}}/.project-gitignore new file mode 100644 index 00000000..67739393 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.project-gitignore @@ -0,0 +1,330 @@ +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Static files: +media/ +static/ +staticfiles/ + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# Environments +.env +.venv +venv/ +ENV/ + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + + +# Provided default Pycharm Run/Debug Configurations should be tracked by git +# In case of local modifications made by Pycharm, use update-index command +# for each changed file, like this: +# git update-index --assume-unchanged .idea/{{cookiecutter.project_slug}}.iml +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + + +### Windows template +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### macOS template +# General +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### SublimeText template +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + + +### Vim template +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] + +# Session +Session.vim + +# Temporary +.netrwhist + +# Auto-generated tag files +tags + +# Misc. Python related +.pytest_cache/ +.ipython/ diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index 0c77e443..9848796b 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -1,5 +1,10 @@ # {{cookiecutter.project_name}} +This project was generated with Cookiecutter_ and the `fastapi-react`_ project template. + +.. _Cookiecutter: https://github.com/audreyr/cookiecutter +.. _`fastapi-react`: https://github.com/Buuntu/fastapi-react + ## Features - **FastAPI** with Python 3.8 @@ -31,12 +36,12 @@ To run the alembic migrations (for the users table): docker-compose run --rm backend alembic upgrade head ``` -And navigate to http://localhost:{{cookiecutter.port}} +And navigate to http://localhost:{{cookiecutter.project_port}} _Note: If you see an Nginx error at first with a `502: Bad Gateway` page, you may have to wait for webpack to build the development server (the nginx container builds much more quickly)._ Auto-generated docs will be at -http://localhost:{{cookiecutter.port}}/api/docs +http://localhost:{{cookiecutter.project_port}}/api/docs ### Rebuilding containers: diff --git a/{{cookiecutter.project_slug}}/backend/app/core/config.py b/{{cookiecutter.project_slug}}/backend/app/core/config.py index 76749904..a3866bb4 100644 --- a/{{cookiecutter.project_slug}}/backend/app/core/config.py +++ b/{{cookiecutter.project_slug}}/backend/app/core/config.py @@ -1,7 +1,10 @@ import os -PROJECT_NAME = "{{cookiecutter.project_name}}" +SECRET_KEY = os.getenv("SECRET_KEY") -SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL") +DATABASE_URL = os.getenv("DATABASE_URL") + +SUPERUSER_EMAIL = os.getenv("SUPERUSER_EMAIL") +SUPERUSER_PASSWORD = os.getenv("SUPERUSER_PASSWORD") API_V1_STR = "/api/v1" diff --git a/{{cookiecutter.project_slug}}/backend/app/core/security.py b/{{cookiecutter.project_slug}}/backend/app/core/security.py index 70f99f34..4709cf19 100644 --- a/{{cookiecutter.project_slug}}/backend/app/core/security.py +++ b/{{cookiecutter.project_slug}}/backend/app/core/security.py @@ -1,13 +1,16 @@ +from datetime import datetime, timedelta + import jwt from fastapi.security import OAuth2PasswordBearer from passlib.context import CryptContext -from datetime import datetime, timedelta + +from app.core import config oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -SECRET_KEY = "{{cookiecutter.secret_key}}" +SECRET_KEY = config.SECRET_KEY ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 diff --git a/{{cookiecutter.project_slug}}/backend/app/db/crud.py b/{{cookiecutter.project_slug}}/backend/app/db/crud.py index f11b3633..a6f35bd2 100644 --- a/{{cookiecutter.project_slug}}/backend/app/db/crud.py +++ b/{{cookiecutter.project_slug}}/backend/app/db/crud.py @@ -7,14 +7,14 @@ def get_user(db: Session, user_id: int): - user = db.query(models.User).filter(models.User.id == user_id).first() + user = db.query(models.User).filter(models.User.id == user_id).one_or_many() if not user: raise HTTPException(status_code=404, detail="User not found") return user def get_user_by_email(db: Session, email: str) -> schemas.UserBase: - return db.query(models.User).filter(models.User.email == email).first() + return db.query(models.User).filter(models.User.email == email).one_or_many() def get_users( diff --git a/{{cookiecutter.project_slug}}/backend/app/db/session.py b/{{cookiecutter.project_slug}}/backend/app/db/session.py index d7e2f6c5..2d5bf7f7 100644 --- a/{{cookiecutter.project_slug}}/backend/app/db/session.py +++ b/{{cookiecutter.project_slug}}/backend/app/db/session.py @@ -4,9 +4,7 @@ from app.core import config -engine = create_engine( - config.SQLALCHEMY_DATABASE_URI, -) +engine = create_engine(config.DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/{{cookiecutter.project_slug}}/backend/app/initial_data.py b/{{cookiecutter.project_slug}}/backend/app/initial_data.py index b41a523d..aefd26eb 100644 --- a/{{cookiecutter.project_slug}}/backend/app/initial_data.py +++ b/{{cookiecutter.project_slug}}/backend/app/initial_data.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +from app.core import config from app.db.session import get_db from app.db.crud import create_user from app.db.schemas import UserCreate @@ -12,8 +13,8 @@ def init() -> None: create_user( db, UserCreate( - email="{{cookiecutter.superuser_email}}", - password="{{cookiecutter.superuser_password}}", + email=config.SUPERUSER_EMAIL, + password=config.SUPERUSER_PASSWORD, is_active=True, is_superuser=True, ), @@ -21,6 +22,6 @@ def init() -> None: if __name__ == "__main__": - print("Creating superuser {{cookiecutter.superuser_email}}") + print(f"Creating superuser {config.SUPERUSER_EMAIL}") init() print("Superuser created") diff --git a/{{cookiecutter.project_slug}}/backend/app/main.py b/{{cookiecutter.project_slug}}/backend/app/main.py index b3f1c58e..1767f27f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/main.py +++ b/{{cookiecutter.project_slug}}/backend/app/main.py @@ -4,7 +4,6 @@ from app.api.api_v1.routers.users import users_router from app.api.api_v1.routers.auth import auth_router -from app.core import config from app.db.session import SessionLocal from app.core.auth import get_current_active_user from app.core.celery_app import celery_app @@ -12,7 +11,7 @@ app = FastAPI( - title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api" + title="{{cookiecutter.project_name}}", docs_url="/api/docs", openapi_url="/api" ) diff --git a/{{cookiecutter.project_slug}}/backend/conftest.py b/{{cookiecutter.project_slug}}/backend/conftest.py index ecd831dc..78e44e29 100644 --- a/{{cookiecutter.project_slug}}/backend/conftest.py +++ b/{{cookiecutter.project_slug}}/backend/conftest.py @@ -12,7 +12,7 @@ def get_test_db_url() -> str: - return f"{config.SQLALCHEMY_DATABASE_URI}_test" + return f"{config.DATABASE_URL}_test" @pytest.fixture diff --git a/{{cookiecutter.project_slug}}/docker-compose.yml b/{{cookiecutter.project_slug}}/docker-compose.yml index 3209ed83..ffdede0f 100644 --- a/{{cookiecutter.project_slug}}/docker-compose.yml +++ b/{{cookiecutter.project_slug}}/docker-compose.yml @@ -5,7 +5,7 @@ services: volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf ports: - - {{cookiecutter.port}}:80 + - {{cookiecutter.project_port}}:80 depends_on: - backend - frontend @@ -18,9 +18,7 @@ services: postgres: image: postgres:12 restart: always - environment: - POSTGRES_USER: {{cookiecutter.postgres_user}} - POSTGRES_PASSWORD: {{cookiecutter.postgres_password}} + env_file: .env ports: - '5432:5432' volumes: @@ -32,10 +30,10 @@ services: dockerfile: Dockerfile command: celery --app app.tasks worker --loglevel=DEBUG -Q main-queue -c 1 - flower: + flower: image: mher/flower command: celery flower --broker=redis://redis:6379/0 --port=5555 - ports: + ports: - 5555:5555 depends_on: - "redis" @@ -49,9 +47,9 @@ services: volumes: - ./backend:/app/:cached - ./.docker/.ipython:/root/.ipython:cached + env_file: .env environment: PYTHONPATH: . - DATABASE_URL: 'postgresql://{{cookiecutter.postgres_user}}:{{cookiecutter.postgres_password}}@postgres:5432/{{cookiecutter.postgres_user}}' depends_on: - "postgres" diff --git a/{{cookiecutter.project_slug}}/frontend/src/config/index.tsx b/{{cookiecutter.project_slug}}/frontend/src/config/index.tsx index ccf59e28..fdeab29c 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/config/index.tsx +++ b/{{cookiecutter.project_slug}}/frontend/src/config/index.tsx @@ -1,3 +1,3 @@ -export const BASE_URL: string = 'http://localhost:{{cookiecutter.port}}'; +export const BASE_URL: string = 'http://localhost:{{cookiecutter.project_port}}'; export const BACKEND_URL: string = - 'http://localhost:{{cookiecutter.port}}/api/v1'; + 'http://localhost:{{cookiecutter.project_port}}/api/v1'; diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/Home.tsx b/{{cookiecutter.project_slug}}/frontend/src/views/Home.tsx index 123e827d..2a239743 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/Home.tsx +++ b/{{cookiecutter.project_slug}}/frontend/src/views/Home.tsx @@ -20,7 +20,7 @@ export const Home: FC = () => { const message = await getMessage(); setMessage(message); } catch (err) { - setError(err); + setError(String(err)); } }; diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/Login.tsx b/{{cookiecutter.project_slug}}/frontend/src/views/Login.tsx index b791a2c1..26298c8a 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/Login.tsx +++ b/{{cookiecutter.project_slug}}/frontend/src/views/Login.tsx @@ -51,7 +51,7 @@ export const Login: FC = () => { setError(err.message); } else { // handle errors thrown from backend - setError(err); + setError(String(err)); } } }; diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/SignUp.tsx b/{{cookiecutter.project_slug}}/frontend/src/views/SignUp.tsx index 140ee36f..0ede11ee 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/SignUp.tsx +++ b/{{cookiecutter.project_slug}}/frontend/src/views/SignUp.tsx @@ -48,7 +48,7 @@ export const SignUp: FC = () => { setError(err.message); } else { // handle errors thrown from backend - setError(err); + setError(String(err)); } } }