diff --git a/cefies/app.py b/cefies/app.py index ec00753..5bfd85b 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(root_path="/api") app.include_router(index_router) app.include_router(auth_router) +app.include_router(profile_router) 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..3e1d6c9 --- /dev/null +++ b/cefies/models/forms/auth.py @@ -0,0 +1,16 @@ +from fastapi import Form, UploadFile, File + +from cefies.models.forms.base import BaseForm + +class RegisterForm(BaseForm): + def __init__( + self, + email: str = Form(...), + name: str = Form(...), + password: str = Form(...), + 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 new file mode 100644 index 0000000..131482a --- /dev/null +++ b/cefies/models/forms/base.py @@ -0,0 +1,3 @@ +class BaseForm: + def to_dict(self): + return self.__dict__.copy() 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/auth.py b/cefies/routes/auth.py index c282077..bce2d33 100644 --- a/cefies/routes/auth.py +++ b/cefies/routes/auth.py @@ -1,10 +1,13 @@ -from typing import Optional -from fastapi import APIRouter, HTTPException, UploadFile, status +import asyncio +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 -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") @@ -25,9 +28,20 @@ async def login(data: LoginData): @router.post("/register") -def register(data: RegisterData): +async def register(form: RegisterForm = Depends()): + loop = asyncio.get_running_loop() + + try: + 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, + 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, @@ -38,8 +52,12 @@ def register(data: RegisterData): 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 = 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() return MessageResponse( 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) 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,