Skip to content

Commit e46b769

Browse files
authored
Add backend users (Buuntu#16)
* add users router and a basic fetch all users test * add helper script for testing and get alembic migrations working with get_users test * add ability to create users and add hashed passwords * fix being able to see the user list * add link to admin page and some more readme docs
1 parent 0a0d61e commit e46b769

29 files changed

+187
-55
lines changed

CONTRIBUTING.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Development
44

5+
### Helper Scripts
56
You can use the helper script `scripts/dev-project.sh` to create a cookiecutter
67
project to test locally. Do this from outside of the root directory to avoid
78
accidentally commiting test builds. For example:
@@ -15,6 +16,11 @@ cd dev-fastapi-react
1516
docker-compose up -d
1617
```
1718

19+
When developing locally, there is also a helper script that will create a cookiecutter directory, build containers, and run tests all from within the root project directory. This can be kind of a tedious process with cookiecutter so this makes it somewhat less painful. From the root `fastapi-react` directory, simply run:
20+
```bash
21+
./scripts/test_local.sh
22+
```
23+
1824
## Pull Requests
1925

2026
Use the general [feature branch
@@ -26,4 +32,8 @@ Try to keep PRs as small and focused as possible. If you are making a big
2632
breaking change in production and don't want to expose half finished
2733
functionality to users, you can use [feature
2834
flags](https://www.martinfowler.com/articles/feature-toggles.html) to work on
29-
this incrementally. A big PR is much less likely to be approved
35+
this incrementally. A big PR is much less likely to be approved
36+
37+
## Which issues to start with?
38+
39+
Start by browsing through the [list of issues](https://github.com/Buuntu/fastapi-react/issues), particularly those flagged as [help wanted](https://github.com/Buuntu/fastapi-react/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)

README.md

+27-13
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
This project serves as a template for bootstrapping a FastAPI project using the
44
following tools:
55

6-
1. **FastAPI** (Python 3.8)
7-
2. **React** (with Typescript)
6+
1. **[FastAPI](https://fastapi.tiangolo.com/)** (Python 3.8)
7+
2. **[React](https://reactjs.org/)** (with Typescript)
88
3. **Postgres**
9-
4. **SqlAlchemy**
10-
5. **Alembic**
11-
6. **Pytest**
12-
7. **Prettier**/**ESLint** (Airbnb style guide)
9+
4. **[SqlAlchemy](https://www.sqlalchemy.org/)**
10+
5. **[Alembic](https://alembic.sqlalchemy.org/en/latest/)**
11+
6. **[Pytest](https://docs.pytest.org/en/latest/)**
12+
7. **[Prettier](https://prettier.io/)**/**[ESLint](https://eslint.org/)**
13+
(Airbnb style guide)
1314
8. **Docker**
1415
9. **Nginx** as a reverse proxy to allow backend/frontend on the same port
15-
10. [MaterialUI](https://material-ui.com/) for styling
16-
11. [react-admin](https://github.com/marmelab/react-admin) for the admin
16+
10. [**MaterialUI**](https://material-ui.com/) for styling
17+
11. [**react-admin**](https://github.com/marmelab/react-admin) for the admin
1718
dashboard
1819

1920
It is meant as a lightweight/React alternative to [FastAPI's official fullstack
@@ -26,7 +27,7 @@ FastAPI.
2627
This does not have any opinions on production settings and leaves that up to the
2728
user.
2829

29-
## Project Setup
30+
## Quick Start
3031

3132
First, install cookiecutter:
3233

@@ -54,14 +55,21 @@ and will create a directory called whatever you set for `project_slug`.
5455

5556
Change into your project directory and then run:
5657

57-
```
58+
```bash
5859
docker-compose up -d
60+
docker-compose run --rm backend alembic upgrade head
5961
```
6062

6163
This will take a little while to build the first time it's run.
6264

6365
Once this finishes you can navigate to the port set during setup (default is
64-
`localhost:8000`), you should see the default create-react-app.
66+
`localhost:8000`), you should see the default create-react-app:
67+
68+
![default create-react-app](assets/create-react-app.png)
69+
70+
*Note: If you see an Nginx error at first with a `502: Bad Gateway` page, you
71+
may have to wait for webpack to build the development server (the nginx
72+
container builds much more quickly).*
6573

6674
The backend docs will be at http://localhost:8000/api/docs by default.
6775

@@ -74,8 +82,14 @@ configurable admin dashboard.
7482

7583
After starting the project, navigate to `http://localhost:8000/admin`. By
7684
default you should see a list of users, which you can edit, add, and delete.
85+
These are all based off of the `users` routes.
7786

7887
![React Admin Dashboard](assets/admin-dashboard.png)
7988

80-
Routes are kept in the `frontend/src/admin` by default to
81-
keep them separate from regular frontend routes.
89+
Routes are kept in the `frontend/src/admin` by default to keep them separate
90+
from regular frontend routes.
91+
92+
## Contributing
93+
94+
Contributing to this project is encouraged. Please read the [Contributing
95+
doc](CONTRIBUTING.md) first.

assets/create-react-app.png

105 KB
Loading

cookiecutter.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"port": "8000",
55
"postgres_user": "postgres",
66
"postgres_password": "password",
7-
"postgres_server": "db",
7+
"postgres_server": "postgres",
88
"postgres_database": "app"
99
}

scripts/test.sh

+6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ cd testing-project
1111
docker-compose build
1212
docker-compose down -v --remove-orphans
1313
docker-compose up -d
14+
# Run migrations first
15+
docker-compose run --rm backend alembic upgrade head
16+
17+
# Backend/frontend tests
1418
./scripts/test.sh
19+
20+
# Cleanup
1521
docker-compose down -v --remove-orphans
1622

1723
# only remove directory if running locally

scripts/test_local.sh

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
cd ..
2+
./fastapi-react/scripts/dev-project.sh
3+
cd dev-fastapi-react
4+
docker-compose down -v --remove-orphans
5+
docker-compose up -d
6+
docker-compose run --rm backend alembic upgrade head
7+
./scripts/test.sh

{{cookiecutter.project_slug}}/README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ The only dependencies for this project should be docker and docker-compose.
2121
Starting the project with hot-reloading enabled
2222
(the first time it will take a while):
2323

24-
```
24+
```bash
2525
docker-compose up -d
2626
```
2727

28+
To run the alembic migrations (for the users table):
29+
```bash
30+
docker-compose run --rm backend alembic upgrade head
31+
```
32+
2833
And navigate to http://localhost:{{cookiecutter.port}}
2934

35+
*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).*
36+
3037
Auto-generated docs will be at
3138
http://localhost:{{cookiecutter.port}}/api/docs
3239

{{cookiecutter.project_slug}}/backend/app/alembic/versions/91979b40eb38_create_users_table.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def upgrade():
2121
'user',
2222
sa.Column('id', sa.Integer, primary_key=True),
2323
sa.Column('email', sa.String(50), nullable=False),
24-
sa.Column('hashed_password', sa.String(50), nullable=False),
24+
sa.Column('hashed_password', sa.String(100), nullable=False),
2525
sa.Column('is_active', sa.Boolean),
2626
)
2727

{{cookiecutter.project_slug}}/backend/app/api/api_v1/api.py

-9
This file was deleted.

{{cookiecutter.project_slug}}/backend/app/api/api_v1/routers/tests/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from fastapi.testclient import TestClient
2+
from app.main import app
3+
4+
client = TestClient(app)
5+
6+
7+
def test_get_users():
8+
response = client.get("/api/v1/users")
9+
assert response.status_code == 200
10+
11+
12+
def test_delete_user():
13+
"""TODO"""
14+
pass
15+
16+
17+
def test_edit_user():
18+
"""TODO"""
19+
pass
20+
21+
22+
def test_get_user():
23+
"""TODO: Gets a single user"""
24+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from fastapi import APIRouter, Request, Depends, Response
2+
3+
from app.db.session import get_db
4+
from app.db.crud import get_users, get_user, create_user, delete_user
5+
from app.db.schemas import UserCreate
6+
7+
users_router = r = APIRouter()
8+
9+
10+
@r.get("/users")
11+
async def users_list(request: Request, response: Response, db=Depends(get_db)):
12+
users = get_users(db)
13+
response.headers['Content-Range'] = f'0-9/{len(users)}'
14+
return users
15+
16+
17+
@r.get("/users/{user_id}")
18+
async def user_details(request: Request, user_id: str, db=Depends(get_db)):
19+
user = get_user(db, user_id)
20+
return user
21+
22+
23+
@r.post("/users")
24+
async def user_create(request: Request, user: UserCreate, db=Depends(get_db)):
25+
return create_user(db, user)
26+
27+
28+
@r.put("/users")
29+
async def user_edit(request: Request, user: UserCreate, db=Depends(get_db)):
30+
return user
31+
32+
33+
@r.delete("/users/${user_id}")
34+
async def user_delete(request: Request, user_id: str, db=Depends(get_db)):
35+
return delete_user(db, user_id)

{{cookiecutter.project_slug}}/backend/app/api/dependencies/__init__.py

Whitespace-only changes.

{{cookiecutter.project_slug}}/backend/app/api/router.py

-5
This file was deleted.

{{cookiecutter.project_slug}}/backend/app/core/config.py

-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,3 @@
55
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL")
66

77
API_V1_STR = '/api/v1'
8-
9-
FRONTEND_SERVER_URL = os.getenv('FRONTEND_SERVER_URL')

{{cookiecutter.project_slug}}/backend/app/db/crud.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
from sqlalchemy.orm import Session
2+
from passlib.context import CryptContext
23

34
from . import models, schemas
45

6+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
7+
8+
9+
def get_password_hash(password: str) -> str:
10+
return pwd_context.hash(password)
11+
12+
13+
def verify_password(plain_password: str, hashed_password: str) -> bool:
14+
return pwd_context.verify(plain_password, hashed_password)
15+
516

617
def get_user(db: Session, user_id: int):
718
return db.query(models.User).filter(models.User.id == user_id).first()
@@ -16,12 +27,19 @@ def get_users(db: Session, skip: int = 0, limit: int = 100):
1627

1728

1829
def create_user(db: Session, user: schemas.UserCreate):
19-
fake_hashed_password = user.password + "notreallyhashed"
30+
hashed_password = get_password_hash(user.password)
2031
db_user = models.User(
2132
email=user.email,
22-
hashed_password=fake_hashed_password
33+
hashed_password=hashed_password
2334
)
2435
db.add(db_user)
2536
db.commit()
2637
db.refresh(db_user)
2738
return db_user
39+
40+
41+
def delete_user(db: Session, user_id: int):
42+
user = db.query(models.User).get(id)
43+
db.delete(user)
44+
db.commit()
45+
return user

{{cookiecutter.project_slug}}/backend/app/db/models.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44

55

66
class User(Base):
7-
__tablename__ = "users"
7+
__tablename__ = "user"
88

99
id = Column(Integer, primary_key=True, index=True)
10-
first_name = Column(String)
11-
last_name = Column(String)
1210
email = Column(String, unique=True, index=True)
1311
hashed_password = Column(String)
1412
is_active = Column(Boolean, default=True)

{{cookiecutter.project_slug}}/backend/app/db/session.py

+10
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,13 @@
1010
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
1111

1212
Base = declarative_base()
13+
14+
15+
# Dependency
16+
def get_db():
17+
db = SessionLocal()
18+
try:
19+
yield db
20+
finally:
21+
db.close()
22+

{{cookiecutter.project_slug}}/backend/app/main.py

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from starlette.requests import Request
33
import uvicorn
44

5+
from app.api.api_v1.routers.users import users_router
56
from app.core import config
67
from app.db.session import SessionLocal
78

@@ -23,6 +24,8 @@ async def db_session_middleware(request: Request, call_next):
2324
async def root():
2425
return {"message": "Hello World"}
2526

27+
# Routers
28+
app.include_router(users_router, prefix="/api/v1", tags=["users"])
2629

2730
if __name__ == "__main__":
2831
uvicorn.run(

{{cookiecutter.project_slug}}/backend/requirements.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ pytest==5.3.4
1010
requests==2.22.0
1111
SQLAlchemy==1.3.12
1212
starlette==0.13.4
13-
uvicorn==0.11.5
13+
uvicorn==0.11.5
14+
passlib==1.7.2
15+
bcrypt==3.1.7

{{cookiecutter.project_slug}}/docker-compose.yml

+4-5
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ services:
66
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
77
ports:
88
- {{cookiecutter.port}}:80
9-
restart: always
109

11-
db:
10+
postgres:
1211
image: postgres:12
1312
restart: always
1413
environment:
@@ -24,13 +23,13 @@ services:
2423
context: backend
2524
dockerfile: Dockerfile
2625
command: python app/main.py
26+
tty: true
2727
volumes:
28-
- ./backend/:/app/:cached
28+
- ./backend:/app/:cached
2929
- ./.docker/.ipython:/root/.ipython:cached
3030
environment:
3131
PYTHONPATH: .
32-
DATABASE_URL: 'postgresql://{{cookiecutter.postgres_user}}:{{cookiecutter.postgres_password}}@db/{{cookiecutter.postgres_user}}'
33-
FRONTEND_SERVER_URL: 'http://localhost:{{cookiecutter.port}}'
32+
DATABASE_URL: 'postgresql://{{cookiecutter.postgres_user}}:{{cookiecutter.postgres_password}}@{{cookiecutter.postgres_server}}:5432/{{cookiecutter.postgres_user}}'
3433
frontend:
3534
build:
3635
context: frontend

{{cookiecutter.project_slug}}/frontend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@types/react-redux": "^7.1.7",
1313
"@types/react-router-dom": "^5.1.3",
1414
"ra-data-json-server": "^3.5.2",
15+
"ra-data-simple-rest": "^3.3.2",
1516
"react": "^16.13.1",
1617
"react-admin": "^3.5.2",
1718
"react-dom": "^16.13.1",

0 commit comments

Comments
 (0)