Skip to content

Commit

Permalink
Merge pull request #4 from Chefies/feat/auth-upw
Browse files Browse the repository at this point in the history
feat: User-Password authentication
  • Loading branch information
Emyr298 authored Jun 8, 2024
2 parents cefe315 + b21b33e commit eadb298
Show file tree
Hide file tree
Showing 15 changed files with 262 additions and 36 deletions.
6 changes: 5 additions & 1 deletion .firebaserc
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{}
{
"projects": {
"default": "chefies-test"
}
}
3 changes: 2 additions & 1 deletion cefies/app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from fastapi import FastAPI

import cefies.internal.firestore # noqa: F401 -- Intended to initialize Firebase
from cefies.routes import index_router
from cefies.routes import index_router, auth_router

app = FastAPI()
app.include_router(index_router)
app.include_router(auth_router)
13 changes: 13 additions & 0 deletions cefies/internal/bucket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import os
import cefies.internal.firestore # noqa: F401L: Intended to initialize firebase

from firebase_admin import storage

assets_bucket = storage.bucket(os.getenv("STORAGE_BUCKET", "chefies-assets"))


def upload_file(content: bytes | str, target_path: str):
blob = assets_bucket.blob(target_path)
blob.upload_from_string(content)

return blob.public_url
17 changes: 17 additions & 0 deletions cefies/models/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Annotated
from pydantic import BaseModel, EmailStr, StringConstraints


class Token(BaseModel):
token: str


class LoginData(BaseModel):
email: EmailStr
password: str


class RegisterData(BaseModel):
email: EmailStr
password: Annotated[str, StringConstraints(min_length=8)]
name: Annotated[str, StringConstraints(min_length=1)]
2 changes: 2 additions & 0 deletions cefies/models/db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from cefies.models.db.recipe import Recipe
from cefies.models.db.user import User
14 changes: 14 additions & 0 deletions cefies/models/db/recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import List
from fireo.typedmodels import TypedModel
from cefies.models.db.user import User


class Recipe(TypedModel):
creator: User
image: str
ingredients: List[str]
steps: List[str]
title: str

class Meta:
collection_name = "recipes"
11 changes: 11 additions & 0 deletions cefies/models/db/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fireo.typedmodels import TypedModel


class User(TypedModel):
name: str
email: str
avatar: str
password: str

class Meta:
collection_name = "users"
6 changes: 6 additions & 0 deletions cefies/models/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class MessageResponse(BaseModel):
error: bool
message: str
6 changes: 5 additions & 1 deletion cefies/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from cefies.routes.index import router as index_router
from cefies.routes.auth import router as auth_router


__all__ = ["index_router"]
__all__ = [
"index_router",
"auth_router",
]
48 changes: 48 additions & 0 deletions cefies/routes/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Optional
from fastapi import APIRouter, HTTPException, UploadFile, status

from cefies.models.auth import LoginData, RegisterData, Token
from cefies.models.db.user import User
from cefies.models.response import MessageResponse
from cefies.security import authenticate_user, create_access_token, get_password_hash


router = APIRouter(prefix="/auth")


@router.post("/login")
async def login(data: LoginData):
user = authenticate_user(data.email, data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)

token = create_access_token(user.key)
return Token(token=token)


@router.post("/register")
def register(data: RegisterData):
existing_user = User.collection.filter(email=data.email).get()
print(existing_user)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email has been used",
)

new_user = User()
new_user.email = data.email
new_user.name = data.name
new_user.password = get_password_hash(data.password)
# TODO: Upload file to object and set avatar
new_user.avatar = ""
new_user.save()

return MessageResponse(
error=False,
message="Successfully registered",
)
70 changes: 58 additions & 12 deletions cefies/security.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,65 @@
from http.client import UNAUTHORIZED
from typing import Annotated
from fastapi import Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
import os
from typing import Annotated, cast
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import bcrypt

from firebase_admin import auth
from cefies.models.db.user import User
from datetime import datetime, timedelta, timezone
import jwt

from cefies.models.firebase import DecodedIdToken
SECRET_KEY = os.getenv("SECRET_KEY", "insecurekey000000000000000000000")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

security = HTTPBearer()
security = OAuth2PasswordBearer("/auth/login")


def get_current_user(token: Annotated[HTTPAuthorizationCredentials, Depends(security)]):
def authenticate_user(email: str, password: str):
user: User | None = User.collection.filter(email=email).get()
if not user:
return None

if not bcrypt.checkpw(password.encode(), user.password.encode()):
return None

return cast(User, user)


def create_access_token(user_id: str, expires_delta: timedelta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)):
expire = datetime.now(timezone.utc) + expires_delta
encoded_jwt = jwt.encode(
{
"sub": user_id,
"exp": expire,
},
SECRET_KEY,
algorithm=ALGORITHM,
)
return encoded_jwt


def get_password_hash(password: str):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()


def get_current_user(token: Annotated[str, Depends(security)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)

try:
decoded_token: dict = auth.verify_id_token(token.credentials)
except Exception:
raise HTTPException(status_code=UNAUTHORIZED, detail="Invalid token")
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str | None = payload.get("sub")
if not user_id:
raise credentials_exception
except jwt.InvalidTokenError:
raise credentials_exception

user = User.collection.get(user_id)
if not user:
raise credentials_exception

return DecodedIdToken(**decoded_token)
return cast(User, user)
8 changes: 7 additions & 1 deletion firebase.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"storage": {
"rules": "storage.rules"
},
"emulators": {
"auth": {
"port": 9099
Expand All @@ -9,6 +12,9 @@
"ui": {
"enabled": true
},
"singleProjectMode": true
"singleProjectMode": true,
"storage": {
"port": 9199
}
}
}
Loading

0 comments on commit eadb298

Please sign in to comment.