Skip to content

Commit 1bf50b4

Browse files
committed
feature/fix: user profile updated route added & service, schema and utils fixes
1 parent ec86348 commit 1bf50b4

File tree

9 files changed

+139
-222
lines changed

9 files changed

+139
-222
lines changed

pkg/errors.py

Lines changed: 0 additions & 181 deletions
This file was deleted.

pkg/utils.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from datetime import datetime
2+
3+
import bcrypt
4+
from fastapi import Cookie, HTTPException, status
5+
from itsdangerous import URLSafeTimedSerializer
6+
7+
from .config import Config
8+
9+
url_safe_timed_serializer = URLSafeTimedSerializer(
10+
secret_key=Config.JWT_SECRET, salt=Config.JWT_SALT
11+
)
12+
13+
14+
def generate_password_hash(password: str) -> str:
15+
salt = bcrypt.gensalt(rounds=Config.BCRYPT_ROUND)
16+
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
17+
18+
19+
def verify_password(password: str, hashed_password: str) -> bool:
20+
return bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8"))
21+
22+
23+
def generate_url_safe_token(data: dict) -> str:
24+
return url_safe_timed_serializer.dumps(data)
25+
26+
27+
def decode_url_safe_token(token: str) -> dict:
28+
return url_safe_timed_serializer.loads(token)
29+
30+
31+
def verify_url_safe_token(token: str) -> str:
32+
data = decode_url_safe_token(token)
33+
34+
user_uid = data.get("user_uid")
35+
expires_at = data.get("expires_at")
36+
37+
if not user_uid or not expires_at:
38+
raise HTTPException(
39+
status_code=status.HTTP_400_BAD_REQUEST,
40+
detail="Invalid access token",
41+
)
42+
43+
if datetime.now().timestamp() > expires_at:
44+
raise HTTPException(
45+
status_code=status.HTTP_400_BAD_REQUEST,
46+
detail="Access token expired",
47+
)
48+
49+
return user_uid
50+
51+
52+
def get_current_user_uid(access_token: str = Cookie(None)) -> str:
53+
if access_token is None:
54+
raise HTTPException(
55+
status_code=status.HTTP_401_UNAUTHORIZED,
56+
detail="Access token required",
57+
)
58+
59+
return verify_url_safe_token(access_token)

src/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from fastapi import FastAPI
22

3-
from pkg.errors import register_all_errors
43
from pkg.middleware import register_middleware
54
from src.auth.routes import auth_router
5+
from src.profile.routes import profile_router
66

77
version = "v1"
88

@@ -31,8 +31,8 @@
3131
)
3232

3333

34-
register_all_errors(app)
3534
register_middleware(app)
3635

3736

3837
app.include_router(auth_router, prefix=f"{version_prefix}/auth", tags=["auth"])
38+
app.include_router(profile_router, prefix=f"{version_prefix}/profile", tags=["profile"])

src/auth/routes.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77

88
from pkg.config import Config
99
from pkg.db import get_session
10-
from pkg.errors import UserAlreadyExists
1110
from pkg.tasks import send_email_task
11+
from pkg.utils import (
12+
decode_url_safe_token,
13+
generate_password_hash,
14+
generate_url_safe_token,
15+
verify_password,
16+
)
1217

1318
from .schemas import (
1419
UserCreateResponseSchema,
@@ -19,12 +24,6 @@
1924
)
2025
from .service import PasswordResetLogService, TokenBlackListService, UserService
2126
from .tasks import create_user_profile_task
22-
from .utils import (
23-
decode_url_safe_token,
24-
generate_password_hash,
25-
generate_url_safe_token,
26-
verify_password,
27-
)
2827

2928
auth_router = APIRouter()
3029

@@ -39,7 +38,10 @@ async def register_user(
3938
session: AsyncSession = Depends(get_session),
4039
):
4140
if await user_service.user_exists(user_data.email, session):
42-
raise UserAlreadyExists
41+
return JSONResponse(
42+
status_code=status.HTTP_400_BAD_REQUEST,
43+
content={"message": "User with email already exists"},
44+
)
4345

4446
user = await user_service.create_user(user_data, session)
4547

src/auth/service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from sqlalchemy import delete, select
44
from sqlmodel.ext.asyncio.session import AsyncSession
55

6+
from pkg.utils import generate_password_hash
67
from src.profile.models import UserProfile
78

89
from .models import PasswordResetLog, TokenBlacklist, User
910
from .schemas import UserCreateSchema
10-
from .utils import generate_password_hash
1111

1212

1313
class UserService:

src/auth/utils.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/profile/routes.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import json
2+
3+
from fastapi import APIRouter, Depends, status
4+
from fastapi.responses import JSONResponse
5+
from sqlmodel.ext.asyncio.session import AsyncSession
6+
7+
from pkg.db import get_session
8+
from pkg.utils import get_current_user_uid
9+
from src.auth.service import UserService
10+
11+
from .schemas import UserProfileResponseSchema, UserProfileUpdateSchema
12+
from .service import UserProfileService
13+
14+
profile_router = APIRouter()
15+
16+
user_service = UserService()
17+
user_profile_service = UserProfileService()
18+
19+
20+
@profile_router.put("/", status_code=status.HTTP_200_OK)
21+
async def update_user_profile(
22+
profile_data: UserProfileUpdateSchema,
23+
session: AsyncSession = Depends(get_session),
24+
user_uid: str = Depends(get_current_user_uid),
25+
):
26+
user_profile = await user_profile_service.get_user_profile_by_user_uid(
27+
user_uid, session
28+
)
29+
user_profile = await user_profile_service.update_user_profile(
30+
user_profile, profile_data.model_dump(), session
31+
)
32+
33+
return JSONResponse(
34+
status_code=status.HTTP_200_OK,
35+
content={
36+
"message": "User profile updated successfully",
37+
"user_profile": UserProfileResponseSchema(
38+
**json.loads(user_profile.model_dump_json())
39+
).model_dump(),
40+
},
41+
)

src/profile/schemas.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,11 @@ class UserProfileUpdateSchema(BaseModel):
66
max_length=500,
77
description="Tell the Bookly community a little about yourself! You can mention your favorite genres, what kinds of books you're currently reading or offering, and whether you're open to lending, borrowing, or selling books. Help others get to know your reading style and what you're looking for!",
88
)
9-
avatar: str = Field(
10-
max_length=200,
11-
description="A URL to an avatar image. Use a service like DiceBear to generate a random avatar: https://avatars.dicebear.com/",
12-
)
139

1410
model_config = {
1511
"json_schema_extra": {
1612
"example": {
1713
"bio": "I love reading science fiction and fantasy novels. I'm currently reading the latest book in the Wheel of Time series and I'm open to lending and borrowing books.",
18-
"avatar": "https://api.dicebear.com/9.x/adventurer-neutral/png?seed=Adrian",
1914
}
2015
}
2116
}

0 commit comments

Comments
 (0)