Skip to content

Commit 529775b

Browse files
committed
crud_base addition, better readme, script to generate initial super user
1 parent a616888 commit 529775b

File tree

13 files changed

+385
-196
lines changed

13 files changed

+385
-196
lines changed
File renamed without changes.

README.md

Lines changed: 147 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,106 @@
11
# FastAPI-boilerplate
2-
A boilerplate for Fastapi
2+
>A template to speed your FastAPI development up.
33
4+
## About
5+
**FastAPI boilerplate** creates an extendable async API using FastAPI, Pydantic V2, SQLAlchemy 2.0 and PostgreSQL:
6+
- [`FastAPI`](https://fastapi.tiangolo.com): modern Python web framework for building APIs
7+
- [`Pydantic V2`](https://docs.pydantic.dev/2.4/): the most widely used data validation library for Python now rewritten in Rust [`(5x to 50x speed improvement)`](https://docs.pydantic.dev/latest/blog/pydantic-v2-alpha/)
8+
- [`SQLAlchemy 2.0`](https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html): Python SQL toolkit and Object Relational Mapper
9+
- [`PostgreSQL`](https://www.postgresql.org): The World's Most Advanced Open Source Relational Database
10+
11+
## Features
12+
- Fully async
13+
- Pydantic V2 and SQLAlchemy 2.0
14+
- User authentication with JWT
15+
- Easily extendable
16+
- Flexible
17+
18+
19+
# Usage
20+
___
21+
## Start by cloning the repository
22+
```sh
23+
git clone https://github.com/igormagalhaesr/FastAPI-boilerplate
24+
```
25+
___
26+
## Requirements
27+
### Packages
28+
Then install poetry:
29+
```sh
30+
pip install poetry
31+
```
32+
33+
In the **src** directory, run to install required packages:
34+
```sh
35+
poetry install
36+
```
37+
38+
### Environment Variables
39+
Then create a .env file:
40+
```sh
41+
touch .env
42+
```
43+
44+
Inside of .env, create the following app settings variables:
45+
```
46+
# ------------- app settings -------------
47+
APP_NAME="Your app name here"
48+
APP_DESCRIPTION="Your app description here"
49+
APP_VERSION="0.1"
50+
CONTACT_NAME="Your name"
51+
CONTACT_EMAIL="Your email"
52+
LICENSE_NAME="The license you picked"
53+
```
54+
55+
For the database ([`if you don't have a database yet, click here`](#running-postgresql-with-docker)), create:
56+
```
57+
# ------------- database -------------
58+
POSTGRES_USER="your_postgres_user"
59+
POSTGRES_PASSWORD="your_password"
60+
POSTGRES_SERVER="your_server" # default localhost
61+
POSTGRES_PORT=5432
62+
POSTGRES_DB="your_db"
63+
```
64+
65+
For crypt:
66+
Start by running
67+
```sh
68+
openssl rand -hex 32
69+
```
70+
71+
And then create in .env:
72+
```
73+
# ------------- crypt -------------
74+
SECRET_KEY= # result of openssl rand -hex 32
75+
ALGORITHM= # pick an algorithm, default HS256
76+
ACCESS_TOKEN_EXPIRE_MINUTES= # minutes until token expires, default 30
77+
```
78+
79+
For tests, create:
80+
```
81+
# ------------- test -------------
82+
TEST_EMAIL="[email protected]"
83+
TEST_USERNAME="testeruser"
84+
TEST_PASSWORD="Str1ng$t"
85+
```
86+
87+
And finally for the first admin user:
88+
```
89+
# ------------- admin -------------
90+
ADMIN_NAME="your_name"
91+
ADMIN_EMAIL="your_email"
92+
ADMIN_USERNAME="your_username"
93+
ADMIN_PASSWORD="your_password"
94+
```
95+
96+
___
497
## Running PostgreSQL with docker:
5-
After installing docker, run:
98+
Install docker if you don't have it yet, then run:
699
```sh
7100
docker pull postgres
8101
```
9102

10-
Then pick the port, name, user and password and replace them:
103+
And pick the port, name, user and password, replacing the fields:
11104
```sh
12105
docker run -d \
13106
-p {PORT}:{PORT} \
@@ -27,11 +120,56 @@ docker run -d \
27120
postgres
28121
```
29122

30-
And create the variables in a .env file in the src folder:
123+
[`If you didn't create the .env variables yet, click here.`](#environment-variables)
124+
125+
___
126+
## Running the api
127+
While in the **src** folder, run to start the application with uvicorn server:
128+
```sh
129+
poetry run uvicorn app.main:app --reload
130+
```
131+
132+
___
133+
## Creating the first superuser:
134+
While in the **src** folder, run (after you started the application at least once to create the tables):
135+
```sh
136+
poetry run python -m scripts.create_first_superuser
137+
```
138+
139+
___
140+
## Database Migrations
141+
Migrations done via [Alembic](https://alembic.sqlalchemy.org/en/latest/):
142+
143+
Whenever you change somethin in the database, in the **src** directory, run to create the script:
144+
```sh
145+
poetry run alembic revision --autogenerate
31146
```
32-
POSTGRES_USER="postgres"
33-
POSTGRES_PASSWORD=1234
34-
POSTGRES_SERVER="localhost"
35-
POSTGRES_PORT=5432
36-
POSTGRES_DB="postgres"
147+
148+
And to actually migrate:
149+
```sh
150+
poetry run alembic upgrade head
37151
```
152+
153+
___
154+
## Testing
155+
TODO
156+
157+
___
158+
# Other stuff
159+
## Contributing
160+
1. Fork it (https://github.com/igormagalhaesr/FastAPI-boilerplate)
161+
2. Create your feature branch (`git checkout -b feature/fooBar`)
162+
3. Test your changes while in the src folder `poetry run python -m pytest`
163+
4. Commit your changes (`git commit -am 'Add some fooBar'`)
164+
5. Push to the branch (`git push origin feature/fooBar`)
165+
6. Create a new Pull Request
166+
167+
## References
168+
TODO
169+
170+
## License
171+
[`MIT`](LICENSE.md)
172+
173+
## Contact
174+
Igor Magalhaes – [@igormagalhaesr](https://twitter.com/igormagalhaesr)[email protected]
175+
[github.com/igormagalhaesr](https://github.com/igormagalhaesr/)

src/app/api/dependencies.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,39 @@
44

55
from sqlalchemy.ext.asyncio import AsyncSession
66
from jose import JWTError, jwt
7-
from fastapi import Depends, HTTPException, status
7+
from fastapi import Depends, HTTPException
88

99
from app.core.database import async_get_db
10-
from app.crud.crud_users import get_user_by_email, get_user_by_username
1110
from app.core.models import TokenData
1211
from app.models.user import User
12+
from app.crud.crud_users import crud_users
13+
from app.api.exceptions import credentials_exception, privileges_exception
1314

1415
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[AsyncSession, Depends(async_get_db)]):
15-
credentials_exception = HTTPException(
16-
status_code=status.HTTP_401_UNAUTHORIZED,
17-
detail="Could not validate credentials",
18-
headers={"WWW-Authenticate": "Bearer"},
19-
)
2016
try:
2117
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
2218
username_or_email: str = payload.get("sub")
2319
if username_or_email is None:
2420
raise credentials_exception
2521
token_data = TokenData(username_or_email=username_or_email)
26-
2722
except JWTError:
2823
raise credentials_exception
2924

3025
if "@" in username_or_email:
31-
user = await get_user_by_email(db=db, email=token_data.username_or_email)
26+
user = await crud_users.get(db=db, email=token_data.username_or_email)
3227
else:
33-
user = await get_user_by_username(db=db, username=token_data.username_or_email)
28+
user = await crud_users.get(db=db, username=token_data.username_or_email)
3429

3530
if user is None:
3631
raise credentials_exception
3732

3833
if user.is_deleted:
39-
raise HTTPException(status_code=400, detai="User deleted")
34+
raise HTTPException(status_code=400, detail="User deleted")
4035

4136
return user
4237

4338
def get_current_superuser(current_user: Annotated[User, Depends(get_current_user)]) -> User:
4439
if not current_user.is_superuser:
45-
raise HTTPException(
46-
status_code=403, detail="The user doesn't have enough privileges"
47-
)
40+
raise privileges_exception
41+
4842
return current_user

src/app/api/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from fastapi import HTTPException, status
2+
3+
credentials_exception = HTTPException(
4+
status_code=status.HTTP_401_UNAUTHORIZED,
5+
detail="Incorrect email, username or password",
6+
headers={"WWW-Authenticate": "Bearer"},
7+
)
8+
9+
privileges_exception = HTTPException(
10+
status_code=403,
11+
detail="The user doesn't have enough privileges"
12+
)

src/app/api/v1/login.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from typing import Annotated
22
from datetime import timedelta
33

4-
from fastapi import Depends, HTTPException, status
4+
from fastapi import Depends
55
from fastapi.security import OAuth2PasswordRequestForm
66
from sqlalchemy.ext.asyncio import AsyncSession
77
import fastapi
8+
89
from app.core.database import async_get_db
910
from app.core.models import Token
10-
from app.core.security import create_access_token, authenticate_user_by_username, authenticate_user_by_email, ACCESS_TOKEN_EXPIRE_MINUTES
11+
from app.core.security import ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token, authenticate_user
12+
from app.api.exceptions import credentials_exception
1113

1214
router = fastapi.APIRouter(tags=["login"])
1315

@@ -16,19 +18,18 @@ async def login_for_access_token(
1618
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
1719
db: AsyncSession = Depends(async_get_db)
1820
):
19-
if "@" in form_data.username:
20-
user = await authenticate_user_by_email(form_data.username, form_data.password, db=db)
21-
else:
22-
user = await authenticate_user_by_username(form_data.username, form_data.password, db=db)
2321

22+
user = await authenticate_user(
23+
username_or_email=form_data.username,
24+
password=form_data.password,
25+
db=db
26+
)
2427
if not user:
25-
raise HTTPException(
26-
status_code=status.HTTP_401_UNAUTHORIZED,
27-
detail="Incorrect email, username or password",
28-
headers={"WWW-Authenticate": "Bearer"},
29-
)
28+
raise credentials_exception
29+
3030
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
3131
access_token = await create_access_token(
3232
data={"sub": user.username}, expires_delta=access_token_expires
3333
)
34+
3435
return {"access_token": access_token, "token_type": "bearer"}

0 commit comments

Comments
 (0)