Skip to content

Commit 44bf1c2

Browse files
committed
restapi with python, fastapi and mongodb
0 parents  commit 44bf1c2

File tree

14 files changed

+365
-0
lines changed

14 files changed

+365
-0
lines changed

.env

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
MONGO_INITDB_ROOT_USERNAME=admin
2+
MONGO_INITDB_ROOT_PASSWORD=password123
3+
MONGO_INITDB_DATABASE=fastapi
4+
5+
DATABASE_URL=mongodb://admin:password123@localhost:6000/fastapi?authSource=admin
6+
7+
ACCESS_TOKEN_EXPIRES_IN=15
8+
REFRESH_TOKEN_EXPIRES_IN=60
9+
JWT_ALGORITHM=RS256
10+
11+
CLIENT_ORIGIN=http://localhost:3000
12+
13+
14+
JWT_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT2dJQkFBSkJBSSs3QnZUS0FWdHVQYzEzbEFkVk94TlVmcWxzMm1SVmlQWlJyVFpjd3l4RVhVRGpNaFZuCi9KVHRsd3h2a281T0pBQ1k3dVE0T09wODdiM3NOU3ZNd2xNQ0F3RUFBUUpBYm5LaENOQ0dOSFZGaHJPQ0RCU0IKdmZ2ckRWUzVpZXAwd2h2SGlBUEdjeWV6bjd0U2RweUZ0NEU0QTNXT3VQOXhqenNjTFZyb1pzRmVMUWlqT1JhUwp3UUloQU84MWl2b21iVGhjRkltTFZPbU16Vk52TGxWTW02WE5iS3B4bGh4TlpUTmhBaUVBbWRISlpGM3haWFE0Cm15QnNCeEhLQ3JqOTF6bVFxU0E4bHUvT1ZNTDNSak1DSVFEbDJxOUdtN0lMbS85b0EyaCtXdnZabGxZUlJPR3oKT21lV2lEclR5MUxaUVFJZ2ZGYUlaUWxMU0tkWjJvdXF4MHdwOWVEejBEWklLVzVWaSt6czdMZHRDdUVDSUVGYwo3d21VZ3pPblpzbnU1clBsTDJjZldLTGhFbWwrUVFzOCtkMFBGdXlnCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t
15+
JWT_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBSSs3QnZUS0FWdHVQYzEzbEFkVk94TlVmcWxzMm1SVgppUFpSclRaY3d5eEVYVURqTWhWbi9KVHRsd3h2a281T0pBQ1k3dVE0T09wODdiM3NOU3ZNd2xNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__
2+
venv/
3+
# .env

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dev:
2+
docker-compose up -d
3+
4+
dev-down:
5+
docker-compose down

app/__init__.py

Whitespace-only changes.

app/config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pydantic import BaseSettings
2+
3+
4+
class Settings(BaseSettings):
5+
DATABASE_URL: str
6+
MONGO_INITDB_DATABASE: str
7+
8+
JWT_PUBLIC_KEY: str
9+
JWT_PRIVATE_KEY: str
10+
REFRESH_TOKEN_EXPIRES_IN: int
11+
ACCESS_TOKEN_EXPIRES_IN: int
12+
JWT_ALGORITHM: str
13+
14+
CLIENT_ORIGIN: str
15+
16+
class Config:
17+
env_file = './.env'
18+
19+
20+
settings = Settings()

app/database.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from pymongo import mongo_client
2+
import pymongo
3+
from app.config import settings
4+
5+
client = mongo_client.MongoClient(settings.DATABASE_URL)
6+
print('Connected to MongoDB...')
7+
8+
db = client[settings.MONGO_INITDB_DATABASE]
9+
User = db.users
10+
User.create_index([("email", pymongo.ASCENDING)], unique=True)

app/main.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from fastapi import FastAPI
2+
from fastapi.middleware.cors import CORSMiddleware
3+
4+
from app.config import settings
5+
from app.routers import auth, user
6+
7+
app = FastAPI()
8+
9+
origins = [
10+
settings.CLIENT_ORIGIN,
11+
]
12+
13+
app.add_middleware(
14+
CORSMiddleware,
15+
allow_origins=origins,
16+
allow_credentials=True,
17+
allow_methods=["*"],
18+
allow_headers=["*"],
19+
)
20+
21+
22+
app.include_router(auth.router, tags=['Auth'], prefix='/api/auth')
23+
app.include_router(user.router, tags=['Users'], prefix='/api/users')
24+
25+
26+
@app.get("/api/healthchecker")
27+
def root():
28+
return {"message": "Welcome to FastAPI with MongoDB"}

app/oauth2.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import base64
2+
from typing import List
3+
from fastapi import Depends, HTTPException, status
4+
from fastapi_jwt_auth import AuthJWT
5+
from pydantic import BaseModel
6+
from bson.objectid import ObjectId
7+
8+
from app.serializers import userEntity
9+
10+
from .database import User
11+
from .config import settings
12+
13+
14+
class Settings(BaseModel):
15+
authjwt_algorithm: str = settings.JWT_ALGORITHM
16+
authjwt_decode_algorithms: List[str] = [settings.JWT_ALGORITHM]
17+
authjwt_token_location: set = {'cookies', 'headers'}
18+
authjwt_access_cookie_key: str = 'access_token'
19+
authjwt_refresh_cookie_key: str = 'refresh_token'
20+
authjwt_cookie_csrf_protect: bool = False
21+
authjwt_public_key: str = base64.b64decode(
22+
settings.JWT_PUBLIC_KEY).decode('utf-8')
23+
authjwt_private_key: str = base64.b64decode(
24+
settings.JWT_PRIVATE_KEY).decode('utf-8')
25+
26+
27+
@AuthJWT.load_config
28+
def get_config():
29+
return Settings()
30+
31+
32+
class NotVerified(Exception):
33+
pass
34+
35+
36+
class UserNotFound(Exception):
37+
pass
38+
39+
40+
def require_user(Authorize: AuthJWT = Depends()):
41+
try:
42+
Authorize.jwt_required()
43+
user_id = Authorize.get_jwt_subject()
44+
user = userEntity(User.find_one({'_id': ObjectId(str(user_id))}))
45+
46+
if not user:
47+
raise UserNotFound('User no longer exist')
48+
49+
if not user["verified"]:
50+
raise NotVerified('You are not verified')
51+
52+
except Exception as e:
53+
error = e.__class__.__name__
54+
print(error)
55+
if error == 'MissingTokenError':
56+
raise HTTPException(
57+
status_code=status.HTTP_401_UNAUTHORIZED, detail='You are not logged in')
58+
if error == 'UserNotFound':
59+
raise HTTPException(
60+
status_code=status.HTTP_401_UNAUTHORIZED, detail='User no longer exist')
61+
if error == 'NotVerified':
62+
raise HTTPException(
63+
status_code=status.HTTP_401_UNAUTHORIZED, detail='Please verify your account')
64+
raise HTTPException(
65+
status_code=status.HTTP_401_UNAUTHORIZED, detail='Token is invalid or has expired')
66+
return user_id

app/routers/auth.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from datetime import datetime, timedelta
2+
from bson.objectid import ObjectId
3+
from fastapi import APIRouter, Request, Response, status, Depends, HTTPException
4+
from pydantic import EmailStr
5+
6+
from app import oauth2
7+
from app.database import User
8+
from app.serializers import userEntity, userResponseEntity
9+
from .. import schemas, utils
10+
from app.oauth2 import AuthJWT
11+
from ..config import settings
12+
13+
14+
router = APIRouter()
15+
ACCESS_TOKEN_EXPIRES_IN = settings.ACCESS_TOKEN_EXPIRES_IN
16+
REFRESH_TOKEN_EXPIRES_IN = settings.REFRESH_TOKEN_EXPIRES_IN
17+
18+
19+
@router.post('/register', status_code=status.HTTP_201_CREATED, response_model=schemas.UserResponse)
20+
async def create_user(payload: schemas.CreateUserSchema):
21+
# Check if user already exist
22+
user = User.find_one({'email': payload.email.lower()})
23+
if user:
24+
raise HTTPException(status_code=status.HTTP_409_CONFLICT,
25+
detail='Account already exist')
26+
# Compare password and passwordConfirm
27+
if payload.password != payload.passwordConfirm:
28+
raise HTTPException(
29+
status_code=status.HTTP_400_BAD_REQUEST, detail='Passwords do not match')
30+
# Hash the password
31+
payload.password = utils.hash_password(payload.password)
32+
del payload.passwordConfirm
33+
payload.role = 'user'
34+
payload.verified = True
35+
payload.email = payload.email.lower()
36+
payload.created_at = datetime.utcnow()
37+
payload.updated_at = payload.created_at
38+
result = User.insert_one(payload.dict())
39+
new_user = userResponseEntity(User.find_one({'_id': result.inserted_id}))
40+
return {"status": "success", "user": new_user}
41+
42+
43+
@router.post('/login')
44+
def login(payload: schemas.LoginUserSchema, response: Response, Authorize: AuthJWT = Depends()):
45+
# Check if the user exist
46+
user = userEntity(User.find_one({'email': payload.email.lower()}))
47+
if not user:
48+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
49+
detail='Incorrect Email or Password')
50+
51+
# Check if user verified his email
52+
if not user['verified']:
53+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
54+
detail='Please verify your email address')
55+
56+
# Check if the password is valid
57+
if not utils.verify_password(payload.password, user['password']):
58+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
59+
detail='Incorrect Email or Password')
60+
61+
# Create access token
62+
access_token = Authorize.create_access_token(
63+
subject=str(user["id"]), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN))
64+
65+
# Create refresh token
66+
refresh_token = Authorize.create_refresh_token(
67+
subject=str(user["id"]), expires_time=timedelta(minutes=REFRESH_TOKEN_EXPIRES_IN))
68+
69+
# Store refresh and access tokens in cookie
70+
response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60,
71+
ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax')
72+
response.set_cookie('refresh_token', refresh_token,
73+
REFRESH_TOKEN_EXPIRES_IN * 60, REFRESH_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax')
74+
response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60,
75+
ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax')
76+
77+
# Send both access
78+
return {'status': 'success', 'access_token': access_token}
79+
80+
81+
@router.get('/refresh')
82+
def refresh_token(response: Response, Authorize: AuthJWT = Depends()):
83+
try:
84+
Authorize.jwt_refresh_token_required()
85+
86+
user_id = Authorize.get_jwt_subject()
87+
if not user_id:
88+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
89+
detail='Could not refresh access token')
90+
user = userEntity(User.find_one({'_id': ObjectId(str(user_id))}))
91+
if not user:
92+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
93+
detail='The user belonging to this token no logger exist')
94+
access_token = Authorize.create_access_token(
95+
subject=str(user["id"]), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN))
96+
except Exception as e:
97+
error = e.__class__.__name__
98+
if error == 'MissingTokenError':
99+
raise HTTPException(
100+
status_code=status.HTTP_400_BAD_REQUEST, detail='Please provide refresh token')
101+
raise HTTPException(
102+
status_code=status.HTTP_400_BAD_REQUEST, detail=error)
103+
104+
response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60,
105+
ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax')
106+
response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60,
107+
ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax')
108+
return {'access_token': access_token}
109+
110+
111+
@router.get('/logout', status_code=status.HTTP_200_OK)
112+
def logout(response: Response, Authorize: AuthJWT = Depends(), user_id: str = Depends(oauth2.require_user)):
113+
Authorize.unset_jwt_cookies()
114+
response.set_cookie('logged_in', '', -1)
115+
116+
return {'status': 'success'}

app/routers/user.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from fastapi import APIRouter, Depends
2+
from bson.objectid import ObjectId
3+
from app.serializers import userResponseEntity
4+
5+
from app.database import User
6+
from .. import schemas, oauth2
7+
8+
router = APIRouter()
9+
10+
11+
@router.get('/me', response_model=schemas.UserResponse)
12+
def get_me(user_id: str = Depends(oauth2.require_user)):
13+
user = userResponseEntity(User.find_one({'_id': ObjectId(str(user_id))}))
14+
return {"status": "success", "user": user}

app/schemas.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from datetime import datetime
2+
from pydantic import BaseModel, EmailStr, constr
3+
4+
5+
class UserBaseSchema(BaseModel):
6+
id: str | None = None
7+
name: str
8+
email: str
9+
photo: str
10+
role: str | None = None
11+
created_at: datetime | None = None
12+
updated_at: datetime | None = None
13+
14+
class Config:
15+
orm_mode = True
16+
17+
18+
class CreateUserSchema(UserBaseSchema):
19+
password: constr(min_length=8)
20+
passwordConfirm: str
21+
verified: bool = False
22+
23+
24+
class LoginUserSchema(BaseModel):
25+
email: EmailStr
26+
password: constr(min_length=8)
27+
28+
29+
class UserResponse(BaseModel):
30+
status: str
31+
user: UserBaseSchema

app/serializers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
def userEntity(user) -> dict:
2+
return {
3+
"id": str(user["_id"]),
4+
"name": user["name"],
5+
"email": user["email"],
6+
"role": user["role"],
7+
"photo": user["photo"],
8+
"verified": user["verified"],
9+
"password": user["password"],
10+
"created_at": user["created_at"],
11+
"updated_at": user["updated_at"]
12+
}
13+
14+
15+
def userResponseEntity(user) -> dict:
16+
return {
17+
"id": str(user["_id"]),
18+
"name": user["name"],
19+
"email": user["email"],
20+
"role": user["role"],
21+
"photo": user["photo"],
22+
"created_at": user["created_at"],
23+
"updated_at": user["updated_at"]
24+
}
25+
26+
27+
def userListEntity(users) -> list:
28+
return [userEntity(user) for user in users]

app/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from passlib.context import CryptContext
2+
3+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
4+
5+
6+
def hash_password(password: str):
7+
return pwd_context.hash(password)
8+
9+
10+
def verify_password(password: str, hashed_password: str):
11+
return pwd_context.verify(password, hashed_password)

docker-compose.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: '3'
2+
services:
3+
mongo:
4+
image: mongo:latest
5+
container_name: mongo
6+
env_file:
7+
- ./.env
8+
environment:
9+
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
10+
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
11+
MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE}
12+
volumes:
13+
- mongo:/data/db
14+
ports:
15+
- '6000:27017'
16+
17+
volumes:
18+
mongo:

0 commit comments

Comments
 (0)