From 63bf45b1666fc4339cdbea3357c3ebead9f055e9 Mon Sep 17 00:00:00 2001 From: Emyr298 Date: Sat, 8 Jun 2024 17:25:25 +0700 Subject: [PATCH 1/4] feat: add form data support on register endpoint --- cefies/models/forms/__init__.py | 0 cefies/models/forms/auth.py | 15 +++++++++++++++ cefies/models/forms/base.py | 3 +++ cefies/routes/auth.py | 16 ++++++++++++---- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 cefies/models/forms/__init__.py create mode 100644 cefies/models/forms/auth.py create mode 100644 cefies/models/forms/base.py diff --git a/cefies/models/forms/__init__.py b/cefies/models/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cefies/models/forms/auth.py b/cefies/models/forms/auth.py new file mode 100644 index 0000000..3c9a3bb --- /dev/null +++ b/cefies/models/forms/auth.py @@ -0,0 +1,15 @@ +from fastapi import Form + +from cefies.models.forms.base import BaseForm + +class RegisterForm(BaseForm): + def __init__( + self, + email: str = Form(...), + name: str = Form(...), + password: str = Form(...), + # TODO file: UploadFile = File(...), + ): + self.email = email + self.name = name + self.password = password diff --git a/cefies/models/forms/base.py b/cefies/models/forms/base.py new file mode 100644 index 0000000..655c4fa --- /dev/null +++ b/cefies/models/forms/base.py @@ -0,0 +1,3 @@ +class BaseForm: + def to_dict(self): + return self.__dict__ diff --git a/cefies/routes/auth.py b/cefies/routes/auth.py index c282077..46d32ff 100644 --- a/cefies/routes/auth.py +++ b/cefies/routes/auth.py @@ -1,6 +1,7 @@ -from typing import Optional -from fastapi import APIRouter, HTTPException, UploadFile, status +from fastapi import APIRouter, HTTPException, status, Depends +from pydantic import ValidationError +from cefies.models.forms.auth import RegisterForm from cefies.models.auth import LoginData, RegisterData, Token from cefies.models.db.user import User from cefies.models.response import MessageResponse @@ -25,9 +26,16 @@ async def login(data: LoginData): @router.post("/register") -def register(data: RegisterData): +def register(data: RegisterForm = Depends()): + try: + data = RegisterData(**data.to_dict()) + except ValidationError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=e.errors(), + ) + existing_user = User.collection.filter(email=data.email).get() - print(existing_user) if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, From fb7a81e76f8022c27dd3b56fdc64f8882bc1d9e3 Mon Sep 17 00:00:00 2001 From: Emyr298 Date: Sat, 8 Jun 2024 17:51:34 +0700 Subject: [PATCH 2/4] feat: add file upload to register --- cefies/models/forms/auth.py | 5 +++-- cefies/models/forms/base.py | 2 +- cefies/routes/auth.py | 14 +++++++++----- cefies/security.py | 5 +++++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/cefies/models/forms/auth.py b/cefies/models/forms/auth.py index 3c9a3bb..3e1d6c9 100644 --- a/cefies/models/forms/auth.py +++ b/cefies/models/forms/auth.py @@ -1,4 +1,4 @@ -from fastapi import Form +from fastapi import Form, UploadFile, File from cefies.models.forms.base import BaseForm @@ -8,8 +8,9 @@ def __init__( email: str = Form(...), name: str = Form(...), password: str = Form(...), - # TODO file: UploadFile = File(...), + avatar: UploadFile = File(...), ): self.email = email self.name = name self.password = password + self.avatar = avatar diff --git a/cefies/models/forms/base.py b/cefies/models/forms/base.py index 655c4fa..131482a 100644 --- a/cefies/models/forms/base.py +++ b/cefies/models/forms/base.py @@ -1,3 +1,3 @@ class BaseForm: def to_dict(self): - return self.__dict__ + return self.__dict__.copy() diff --git a/cefies/routes/auth.py b/cefies/routes/auth.py index 46d32ff..eb3ca77 100644 --- a/cefies/routes/auth.py +++ b/cefies/routes/auth.py @@ -5,7 +5,8 @@ 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 +from cefies.security import authenticate_user, create_access_token, get_password_hash, get_hash_sha256 +from cefies.internal import bucket router = APIRouter(prefix="/auth") @@ -26,9 +27,11 @@ async def login(data: LoginData): @router.post("/register") -def register(data: RegisterForm = Depends()): +async def register(form: RegisterForm = Depends()): try: - data = RegisterData(**data.to_dict()) + data_dict = form.to_dict() + data_dict.pop("avatar", None) + data = RegisterData(**data_dict) except ValidationError as e: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, @@ -46,8 +49,9 @@ def register(data: RegisterForm = Depends()): 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 = "" + avatar_content = await form.avatar.read() + avatar_url = bucket.upload_file(avatar_content, get_hash_sha256(avatar_content)) + new_user.avatar = avatar_url new_user.save() return MessageResponse( diff --git a/cefies/security.py b/cefies/security.py index 945cf7c..69ce00d 100644 --- a/cefies/security.py +++ b/cefies/security.py @@ -3,6 +3,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer import bcrypt +import hashlib from cefies.models.db.user import User from datetime import datetime, timedelta, timezone @@ -43,6 +44,10 @@ def get_password_hash(password: str): return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() +def get_hash_sha256(content: bytes): + return hashlib.sha256(content).hexdigest() + + def get_current_user(token: Annotated[str, Depends(security)]): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, From 348a9cad33e478fa63aca1f698d7ef4ad3ea864b Mon Sep 17 00:00:00 2001 From: Emyr298 Date: Sun, 9 Jun 2024 13:04:24 +0700 Subject: [PATCH 3/4] feat: implement profile endpoint --- cefies/app.py | 3 ++- cefies/models/profile.py | 11 +++++++++++ cefies/routes/__init__.py | 3 ++- cefies/routes/profile.py | 26 ++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 cefies/models/profile.py create mode 100644 cefies/routes/profile.py diff --git a/cefies/app.py b/cefies/app.py index c2b586e..f05f4da 100644 --- a/cefies/app.py +++ b/cefies/app.py @@ -1,8 +1,9 @@ from fastapi import FastAPI import cefies.internal.firestore # noqa: F401 -- Intended to initialize Firebase -from cefies.routes import index_router, auth_router +from cefies.routes import index_router, auth_router, profile_router app = FastAPI() app.include_router(index_router) app.include_router(auth_router) +app.include_router(profile_router) diff --git a/cefies/models/profile.py b/cefies/models/profile.py new file mode 100644 index 0000000..58250c1 --- /dev/null +++ b/cefies/models/profile.py @@ -0,0 +1,11 @@ +from typing import Annotated +from pydantic import BaseModel, StringConstraints + + +class ProfileData(BaseModel): + email: str + name: str + avatar: str + +class ChangePasswordData(BaseModel): + password: Annotated[str, StringConstraints(min_length=8)] diff --git a/cefies/routes/__init__.py b/cefies/routes/__init__.py index d92025f..ba313d5 100644 --- a/cefies/routes/__init__.py +++ b/cefies/routes/__init__.py @@ -1,8 +1,9 @@ from cefies.routes.index import router as index_router from cefies.routes.auth import router as auth_router - +from cefies.routes.profile import router as profile_router __all__ = [ "index_router", "auth_router", + "profile_router", ] diff --git a/cefies/routes/profile.py b/cefies/routes/profile.py new file mode 100644 index 0000000..49f4bde --- /dev/null +++ b/cefies/routes/profile.py @@ -0,0 +1,26 @@ +from typing import Annotated +from fastapi import APIRouter, Depends +from fastapi.responses import JSONResponse + +from cefies.models.db.user import User +from cefies.models.profile import ProfileData, ChangePasswordData +from cefies.security import get_current_user, get_password_hash + + +router = APIRouter(prefix="/profile") + + +@router.get("/") +def get_profile( + user: Annotated[User, Depends(get_current_user)], +): + return ProfileData(**user.to_dict()) + +@router.patch("/password") +def change_password( + user: Annotated[User, Depends(get_current_user)], + data: ChangePasswordData, +): + user.password = get_password_hash(data.password) + user.save() + return JSONResponse(content={"detail": "password changed"}, status_code=200) From f9f8c0e892c4746c809e3d27270fe71f8d85e6d7 Mon Sep 17 00:00:00 2001 From: Emyr298 Date: Mon, 10 Jun 2024 22:29:24 +0700 Subject: [PATCH 4/4] fix: upload file blocking --- cefies/routes/auth.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cefies/routes/auth.py b/cefies/routes/auth.py index eb3ca77..bce2d33 100644 --- a/cefies/routes/auth.py +++ b/cefies/routes/auth.py @@ -1,3 +1,4 @@ +import asyncio from fastapi import APIRouter, HTTPException, status, Depends from pydantic import ValidationError @@ -28,6 +29,8 @@ async def login(data: LoginData): @router.post("/register") async def register(form: RegisterForm = Depends()): + loop = asyncio.get_running_loop() + try: data_dict = form.to_dict() data_dict.pop("avatar", None) @@ -50,7 +53,10 @@ async def register(form: RegisterForm = Depends()): new_user.name = data.name new_user.password = get_password_hash(data.password) avatar_content = await form.avatar.read() - avatar_url = bucket.upload_file(avatar_content, get_hash_sha256(avatar_content)) + avatar_url = await loop.run_in_executor( + None, + lambda: bucket.upload_file(avatar_content, get_hash_sha256(avatar_content)) + ) new_user.avatar = avatar_url new_user.save()