├── .gitignore ├── LICENSE ├── README.md ├── backend ├── __init__.py ├── database.py ├── main.py ├── models.py ├── schemas.py └── services.py └── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── index.html ├── manifest.json └── robots.txt └── src ├── App.jsx ├── components ├── ErrorMessage.jsx ├── Header.jsx ├── LeadModal.jsx ├── Login.jsx ├── Register.jsx └── Table.jsx ├── context └── UserContext.jsx └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 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 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | database.db 132 | 133 | /node_modules 134 | 135 | /build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Francis Ali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-fastapi -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixfwa/react-fastapi/8150226c6a91dc8df9518a5354b02af4976a4ed7/backend/__init__.py -------------------------------------------------------------------------------- /backend/database.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as _sql 2 | import sqlalchemy.ext.declarative as _declarative 3 | import sqlalchemy.orm as _orm 4 | 5 | DATABASE_URL = "sqlite:///./database.db" 6 | 7 | engine = _sql.create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) 8 | 9 | SessionLocal = _orm.sessionmaker(autocommit=False, autoflush=False, bind=engine) 10 | 11 | Base = _declarative.declarative_base() 12 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import fastapi as _fastapi 3 | import fastapi.security as _security 4 | 5 | import sqlalchemy.orm as _orm 6 | 7 | import services as _services, schemas as _schemas 8 | 9 | app = _fastapi.FastAPI() 10 | 11 | 12 | @app.post("/api/users") 13 | async def create_user( 14 | user: _schemas.UserCreate, db: _orm.Session = _fastapi.Depends(_services.get_db) 15 | ): 16 | db_user = await _services.get_user_by_email(user.email, db) 17 | if db_user: 18 | raise _fastapi.HTTPException(status_code=400, detail="Email already in use") 19 | 20 | user = await _services.create_user(user, db) 21 | 22 | return await _services.create_token(user) 23 | 24 | 25 | @app.post("/api/token") 26 | async def generate_token( 27 | form_data: _security.OAuth2PasswordRequestForm = _fastapi.Depends(), 28 | db: _orm.Session = _fastapi.Depends(_services.get_db), 29 | ): 30 | user = await _services.authenticate_user(form_data.username, form_data.password, db) 31 | 32 | if not user: 33 | raise _fastapi.HTTPException(status_code=401, detail="Invalid Credentials") 34 | 35 | return await _services.create_token(user) 36 | 37 | 38 | @app.get("/api/users/me", response_model=_schemas.User) 39 | async def get_user(user: _schemas.User = _fastapi.Depends(_services.get_current_user)): 40 | return user 41 | 42 | 43 | @app.post("/api/leads", response_model=_schemas.Lead) 44 | async def create_lead( 45 | lead: _schemas.LeadCreate, 46 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 47 | db: _orm.Session = _fastapi.Depends(_services.get_db), 48 | ): 49 | return await _services.create_lead(user=user, db=db, lead=lead) 50 | 51 | 52 | @app.get("/api/leads", response_model=List[_schemas.Lead]) 53 | async def get_leads( 54 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 55 | db: _orm.Session = _fastapi.Depends(_services.get_db), 56 | ): 57 | return await _services.get_leads(user=user, db=db) 58 | 59 | 60 | @app.get("/api/leads/{lead_id}", status_code=200) 61 | async def get_lead( 62 | lead_id: int, 63 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 64 | db: _orm.Session = _fastapi.Depends(_services.get_db), 65 | ): 66 | return await _services.get_lead(lead_id, user, db) 67 | 68 | 69 | @app.delete("/api/leads/{lead_id}", status_code=204) 70 | async def delete_lead( 71 | lead_id: int, 72 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 73 | db: _orm.Session = _fastapi.Depends(_services.get_db), 74 | ): 75 | await _services.delete_lead(lead_id, user, db) 76 | return {"message", "Successfully Deleted"} 77 | 78 | 79 | @app.put("/api/leads/{lead_id}", status_code=200) 80 | async def update_lead( 81 | lead_id: int, 82 | lead: _schemas.LeadCreate, 83 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 84 | db: _orm.Session = _fastapi.Depends(_services.get_db), 85 | ): 86 | await _services.update_lead(lead_id, lead, user, db) 87 | return {"message", "Successfully Updated"} 88 | 89 | 90 | @app.get("/api") 91 | async def root(): 92 | return {"message": "Awesome Leads Manager"} -------------------------------------------------------------------------------- /backend/models.py: -------------------------------------------------------------------------------- 1 | import datetime as _dt 2 | 3 | import sqlalchemy as _sql 4 | import sqlalchemy.orm as _orm 5 | import passlib.hash as _hash 6 | 7 | import database as _database 8 | 9 | 10 | class User(_database.Base): 11 | __tablename__ = "users" 12 | id = _sql.Column(_sql.Integer, primary_key=True, index=True) 13 | email = _sql.Column(_sql.String, unique=True, index=True) 14 | hashed_password = _sql.Column(_sql.String) 15 | 16 | leads = _orm.relationship("Lead", back_populates="owner") 17 | 18 | def verify_password(self, password: str): 19 | return _hash.bcrypt.verify(password, self.hashed_password) 20 | 21 | 22 | class Lead(_database.Base): 23 | __tablename__ = "leads" 24 | id = _sql.Column(_sql.Integer, primary_key=True, index=True) 25 | owner_id = _sql.Column(_sql.Integer, _sql.ForeignKey("users.id")) 26 | first_name = _sql.Column(_sql.String, index=True) 27 | last_name = _sql.Column(_sql.String, index=True) 28 | email = _sql.Column(_sql.String, index=True) 29 | company = _sql.Column(_sql.String, index=True, default="") 30 | note = _sql.Column(_sql.String, default="") 31 | date_created = _sql.Column(_sql.DateTime, default=_dt.datetime.utcnow) 32 | date_last_updated = _sql.Column(_sql.DateTime, default=_dt.datetime.utcnow) 33 | 34 | owner = _orm.relationship("User", back_populates="leads") 35 | -------------------------------------------------------------------------------- /backend/schemas.py: -------------------------------------------------------------------------------- 1 | import datetime as _dt 2 | 3 | import pydantic as _pydantic 4 | 5 | 6 | class _UserBase(_pydantic.BaseModel): 7 | email: str 8 | 9 | 10 | class UserCreate(_UserBase): 11 | hashed_password: str 12 | 13 | class Config: 14 | orm_mode = True 15 | 16 | 17 | class User(_UserBase): 18 | id: int 19 | 20 | class Config: 21 | orm_mode = True 22 | 23 | 24 | class _LeadBase(_pydantic.BaseModel): 25 | first_name: str 26 | last_name: str 27 | email: str 28 | company: str 29 | note: str 30 | 31 | 32 | class LeadCreate(_LeadBase): 33 | pass 34 | 35 | 36 | class Lead(_LeadBase): 37 | id: int 38 | owner_id: int 39 | date_created: _dt.datetime 40 | date_last_updated: _dt.datetime 41 | 42 | class Config: 43 | orm_mode = True 44 | -------------------------------------------------------------------------------- /backend/services.py: -------------------------------------------------------------------------------- 1 | import fastapi as _fastapi 2 | import fastapi.security as _security 3 | import jwt as _jwt 4 | import datetime as _dt 5 | import sqlalchemy.orm as _orm 6 | import passlib.hash as _hash 7 | 8 | import database as _database, models as _models, schemas as _schemas 9 | 10 | oauth2schema = _security.OAuth2PasswordBearer(tokenUrl="/api/token") 11 | 12 | JWT_SECRET = "myjwtsecret" 13 | 14 | 15 | def create_database(): 16 | return _database.Base.metadata.create_all(bind=_database.engine) 17 | 18 | 19 | def get_db(): 20 | db = _database.SessionLocal() 21 | try: 22 | yield db 23 | finally: 24 | db.close() 25 | 26 | 27 | async def get_user_by_email(email: str, db: _orm.Session): 28 | return db.query(_models.User).filter(_models.User.email == email).first() 29 | 30 | 31 | async def create_user(user: _schemas.UserCreate, db: _orm.Session): 32 | user_obj = _models.User( 33 | email=user.email, hashed_password=_hash.bcrypt.hash(user.hashed_password) 34 | ) 35 | db.add(user_obj) 36 | db.commit() 37 | db.refresh(user_obj) 38 | return user_obj 39 | 40 | 41 | async def authenticate_user(email: str, password: str, db: _orm.Session): 42 | user = await get_user_by_email(db=db, email=email) 43 | 44 | if not user: 45 | return False 46 | 47 | if not user.verify_password(password): 48 | return False 49 | 50 | return user 51 | 52 | 53 | async def create_token(user: _models.User): 54 | user_obj = _schemas.User.from_orm(user) 55 | 56 | token = _jwt.encode(user_obj.dict(), JWT_SECRET) 57 | 58 | return dict(access_token=token, token_type="bearer") 59 | 60 | 61 | async def get_current_user( 62 | db: _orm.Session = _fastapi.Depends(get_db), 63 | token: str = _fastapi.Depends(oauth2schema), 64 | ): 65 | try: 66 | payload = _jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) 67 | user = db.query(_models.User).get(payload["id"]) 68 | except: 69 | raise _fastapi.HTTPException( 70 | status_code=401, detail="Invalid Email or Password" 71 | ) 72 | 73 | return _schemas.User.from_orm(user) 74 | 75 | 76 | async def create_lead(user: _schemas.User, db: _orm.Session, lead: _schemas.LeadCreate): 77 | lead = _models.Lead(**lead.dict(), owner_id=user.id) 78 | db.add(lead) 79 | db.commit() 80 | db.refresh(lead) 81 | return _schemas.Lead.from_orm(lead) 82 | 83 | 84 | async def get_leads(user: _schemas.User, db: _orm.Session): 85 | leads = db.query(_models.Lead).filter_by(owner_id=user.id) 86 | 87 | return list(map(_schemas.Lead.from_orm, leads)) 88 | 89 | 90 | async def _lead_selector(lead_id: int, user: _schemas.User, db: _orm.Session): 91 | lead = ( 92 | db.query(_models.Lead) 93 | .filter_by(owner_id=user.id) 94 | .filter(_models.Lead.id == lead_id) 95 | .first() 96 | ) 97 | 98 | if lead is None: 99 | raise _fastapi.HTTPException(status_code=404, detail="Lead does not exist") 100 | 101 | return lead 102 | 103 | 104 | async def get_lead(lead_id: int, user: _schemas.User, db: _orm.Session): 105 | lead = await _lead_selector(lead_id=lead_id, user=user, db=db) 106 | 107 | return _schemas.Lead.from_orm(lead) 108 | 109 | 110 | async def delete_lead(lead_id: int, user: _schemas.User, db: _orm.Session): 111 | lead = await _lead_selector(lead_id, user, db) 112 | 113 | db.delete(lead) 114 | db.commit() 115 | 116 | async def update_lead(lead_id: int, lead: _schemas.LeadCreate, user: _schemas.User, db: _orm.Session): 117 | lead_db = await _lead_selector(lead_id, user, db) 118 | 119 | lead_db.first_name = lead.first_name 120 | lead_db.last_name = lead.last_name 121 | lead_db.email = lead.email 122 | lead_db.company = lead.company 123 | lead_db.note = lead.note 124 | lead_db.date_last_updated = _dt.datetime.utcnow() 125 | 126 | db.commit() 127 | db.refresh(lead_db) 128 | 129 | return _schemas.Lead.from_orm(lead_db) 130 | 131 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/ 3 | .vscode/ 4 | node_modules/ 5 | build 6 | .DS_Store 7 | *.tgz 8 | my-app* 9 | template/src/__tests__/__snapshots__/ 10 | lerna-debug.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | /.changelog 15 | .npm/ 16 | yarn.lock -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.14.1", 7 | "@testing-library/react": "^11.2.7", 8 | "@testing-library/user-event": "^12.8.3", 9 | "bulma": "^0.9.3", 10 | "moment": "^2.29.1", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-scripts": "4.0.3", 14 | "web-vitals": "^1.1.2" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "proxy": "http://localhost:8000" 41 | } 42 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 |{message}
5 | ); 6 | 7 | export default ErrorMessage; 8 | -------------------------------------------------------------------------------- /frontend/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | import { UserContext } from "../context/UserContext"; 4 | 5 | const Header = ({ title }) => { 6 | const [token, setToken] = useContext(UserContext); 7 | 8 | const handleLogout = () => { 9 | setToken(null); 10 | }; 11 | 12 | return ( 13 |First Name | 86 |Last Name | 87 |Company | 88 |Note | 90 |Last Updated | 91 |Actions | 92 ||
---|---|---|---|---|---|---|
{lead.first_name} | 98 |{lead.last_name} | 99 |{lead.company} | 100 |{lead.email} | 101 |{lead.note} | 102 |{moment(lead.date_last_updated).format("MMM Do YY")} | 103 |104 | 110 | 116 | | 117 |
Loading
123 | )} 124 | > 125 | ); 126 | }; 127 | 128 | export default Table; 129 | -------------------------------------------------------------------------------- /frontend/src/context/UserContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useEffect, useState } from "react"; 2 | 3 | export const UserContext = createContext(); 4 | 5 | export const UserProvider = (props) => { 6 | const [token, setToken] = useState(localStorage.getItem("awesomeLeadsToken")); 7 | 8 | useEffect(() => { 9 | const fetchUser = async () => { 10 | const requestOptions = { 11 | method: "GET", 12 | headers: { 13 | "Content-Type": "application/json", 14 | Authorization: "Bearer " + token, 15 | }, 16 | }; 17 | 18 | const response = await fetch("/api/users/me", requestOptions); 19 | 20 | if (!response.ok) { 21 | setToken(null); 22 | } 23 | localStorage.setItem("awesomeLeadsToken", token); 24 | }; 25 | fetchUser(); 26 | }, [token]); 27 | 28 | return ( 29 |