Skip to content
Open

Test6 #211

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a94638d
replaced git pushes to be located to my github repo
ipl2 Jul 28, 2025
c9f5f19
added file to trigger github actions workflow
ipl2 Jul 28, 2025
60275a9
changing versin pin to install lastest available version
ipl2 Jul 28, 2025
be85c8b
forgot to remove old version in a code line
ipl2 Jul 28, 2025
7d8d936
fixed some versions for Trivy to pass
ipl2 Jul 28, 2025
9d62543
triggering a rerun of tests. deleted workflow run
ipl2 Jul 28, 2025
45b91db
changing version to pass trivy
ipl2 Jul 28, 2025
1d1be95
fixing duplication for test to pass on github actions
ipl2 Jul 28, 2025
8cf2071
forgot to change _ to i
ipl2 Jul 28, 2025
7c44cf1
changed some code to fix issues in instructor video
ipl2 Jul 29, 2025
17ec3f9
added a new route for admins/managers to separate from the users only…
ipl2 Jul 29, 2025
ff7363e
added class for admins/managers to separate from users update class. …
ipl2 Jul 29, 2025
61d800c
added class method for managers/admin when making updates
ipl2 Jul 29, 2025
c1d2a6e
fixed fake_email duplications that was occurring
ipl2 Jul 29, 2025
6fe7eed
added retry logic and set a max of 5 attempts.
ipl2 Jul 29, 2025
f28c851
added code to the update method to ensure users cannot update sensiti…
ipl2 Jul 29, 2025
ad3b7fa
added a new route that will allow the admin and maangers to rest the …
ipl2 Jul 29, 2025
7953203
added missing logging to detail users where they may have failed in t…
ipl2 Jul 29, 2025
3cf9b7f
added new route for registration process of users
ipl2 Jul 30, 2025
3fdbb64
properly injecting the correct instance of EmailService
ipl2 Jul 30, 2025
9d151f9
cleaned up code
ipl2 Jul 30, 2025
b2a97f0
added required content for readme.md
ipl2 Jul 30, 2025
2219776
added testing for behavior of unlocking users via admin, manager, and…
ipl2 Jul 30, 2025
b3726fe
added additional endpoints to unlock_user_account route
ipl2 Jul 31, 2025
dd4754d
adding tests for resetting behavior for manager, admin, and usr
ipl2 Jul 31, 2025
0cba848
cleaned up code
ipl2 Jul 31, 2025
308dd2c
fixing up readme file
ipl2 Jul 31, 2025
178e4ae
fixed code for tests to pass
ipl2 Jul 31, 2025
e60e2d5
added testing for retry logic behavior for succesful and unsuccessful…
ipl2 Jul 31, 2025
63bc102
adding to readme
ipl2 Jul 31, 2025
5fd4653
adding to readme file
ipl2 Jul 31, 2025
0cd200c
adding testing for user and admin behaviors of updating allowed fields
ipl2 Jul 31, 2025
a662488
cleaned up code
ipl2 Jul 31, 2025
4212c25
fixing readme file
ipl2 Jul 31, 2025
72c2122
added fixture for app. fixed async_client fixture to use app dependency
ipl2 Jul 31, 2025
e3a6c82
added tests for testing behavior of registering users with valid emai…
ipl2 Jul 31, 2025
953f2bd
cleaned up code
ipl2 Jul 31, 2025
55011a6
fixed code to use the added fixture of mock_email_service
ipl2 Jul 31, 2025
a446e5c
isue with mock testing with email vs email using stmp. adjusted githu…
ipl2 Jul 31, 2025
f26a093
forgot import
ipl2 Jul 31, 2025
b325141
added email_service to the admin_update_user route
ipl2 Aug 4, 2025
86db374
added is_professional to user UserUpdateAdmin
ipl2 Aug 4, 2025
9c468ac
added a send_email and a notification email for changed on profession…
ipl2 Aug 4, 2025
e1e0fca
added behavior to check and send when professional status is upgraded…
ipl2 Aug 4, 2025
93d8f18
forgot to add email_service
ipl2 Aug 4, 2025
7b7f5e0
forgot to add email_service here as well
ipl2 Aug 4, 2025
fed98ff
had to add mocking since I reached max email testing. SMTP is optiona…
ipl2 Aug 4, 2025
53c7a28
added is_professional to UserUpdateAdmin
ipl2 Aug 5, 2025
fb9e245
added a staticmethod for updating status allowed only by manafer and …
ipl2 Aug 5, 2025
d8087a7
added fixture for other_user and removed scoped_session
ipl2 Aug 5, 2025
947cc7e
debugged issue with duplicate emails. nickname and emails needded to …
ipl2 Aug 5, 2025
f18850a
added testing for denying status update by nonadmins/nonmanagers
ipl2 Aug 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
jobs:
test:
runs-on: ubuntu-latest
env:
CI: true
strategy:
matrix:
python-version: [3.10.12] # Define Python versions here
Expand Down Expand Up @@ -70,15 +72,15 @@ jobs:
uses: docker/build-push-action@v5
with:
push: true
tags: kaw393939/wis_club_api:${{ github.sha }} # Uses the Git SHA for tagging
tags: ipl2/user_management_final:${{ github.sha }} # Uses the Git SHA for tagging
platforms: linux/amd64,linux/arm64 # Multi-platform support
cache-from: type=registry,ref=kaw393939/wis_club_api:cache
cache-from: type=registry,ref=ipl2/user_management_final:cache
cache-to: type=inline,mode=max

- name: Scan the Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'kaw393939/wis_club_api:${{ github.sha }}'
image-ref: 'ipl2/user_management_final:${{ github.sha }}'
format: 'table'
exit-code: '1' # Fail the job if vulnerabilities are found
ignore-unfixed: true
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ WORKDIR /myapp
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& apt-get install -y libc-bin=2.36-9+deb12u7 \
libc-bin \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

Expand All @@ -30,7 +30,7 @@ RUN python -m venv /.venv \
FROM python:3.12-slim-bookworm as final

# Upgrade libc-bin in the final stage to ensure security patch is applied
RUN apt-get update && apt-get install -y libc-bin=2.36-9+deb12u7 \
RUN apt-get update && apt-get install -y libc-bin \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

Expand Down
62 changes: 60 additions & 2 deletions app/routers/user_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from app.dependencies import get_current_user, get_db, get_email_service, require_role
from app.schemas.pagination_schema import EnhancedPagination
from app.schemas.token_schema import TokenResponse
from app.schemas.user_schemas import LoginRequest, UserBase, UserCreate, UserListResponse, UserResponse, UserUpdate
from app.schemas.user_schemas import LoginRequest, UserBase, UserCreate, UserListResponse, UserResponse, UserUpdatePublic, UserUpdateAdmin
from app.services.user_service import UserService
from app.services.jwt_service import create_access_token
from app.utils.link_generation import create_user_links, generate_pagination_links
Expand Down Expand Up @@ -78,15 +78,17 @@ async def get_user(user_id: UUID, request: Request, db: AsyncSession = Depends(g
# This approach not only ensures that the API is secure and efficient but also promotes a better client
# experience by adhering to REST principles and providing self-discoverable operations.

# route only for users
@router.put("/users/{user_id}", response_model=UserResponse, name="update_user", tags=["User Management Requires (Admin or Manager Roles)"])
async def update_user(user_id: UUID, user_update: UserUpdate, request: Request, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))):
async def update_user(user_id: UUID, user_update: UserUpdatePublic, request: Request, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))):
"""
Update user information.

- **user_id**: UUID of the user to update.
- **user_update**: UserUpdate model with updated user information.
"""
user_data = user_update.model_dump(exclude_unset=True)

updated_user = await UserService.update(db, user_id, user_data)
if not updated_user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
Expand All @@ -108,6 +110,36 @@ async def update_user(user_id: UUID, user_update: UserUpdate, request: Request,
links=create_user_links(updated_user.id, request)
)

# adding a new route for just admins and managers
@router.put("/admin/users/{user_id}", response_model=UserResponse, tags=["Admin and Manager Only"])
async def admin_update_user(
user_id: UUID,
update_data: UserUpdateAdmin,
request: Request,
session: AsyncSession = Depends(get_db),
email_service: EmailService = Depends(get_email_service),
token: str = Depends(oauth2_scheme),
current_user: dict = Depends(require_role(["ADMIN", "MANAGER"])),
):
updated_user = await UserService.admin_update(session, user_id, update_data.model_dump(), email_service)
if not updated_user:
raise HTTPException(status_code=404, detail="User not found or update failed")
return UserResponse.model_construct(
id=updated_user.id,
bio=updated_user.bio,
first_name=updated_user.first_name,
last_name=updated_user.last_name,
nickname=updated_user.nickname,
email=updated_user.email,
role=updated_user.role,
last_login_at=updated_user.last_login_at,
profile_picture_url=updated_user.profile_picture_url,
github_profile_url=updated_user.github_profile_url,
linkedin_profile_url=updated_user.linkedin_profile_url,
created_at=updated_user.created_at,
updated_at=updated_user.updated_at,
links=create_user_links(updated_user.id, request)
)

@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT, name="delete_user", tags=["User Management Requires (Admin or Manager Roles)"])
async def delete_user(user_id: UUID, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))):
Expand Down Expand Up @@ -164,6 +196,24 @@ async def create_user(user: UserCreate, request: Request, db: AsyncSession = Dep
links=create_user_links(created_user.id, request)
)

# adding a new route to only admins-only to unlock a locked user account
@router.post("/admin/users/{user_id}/unlock", status_code=status.HTTP_200_OK, tags=["Admin and Manager Only"])
async def unlock_user_account(
user_id: UUID,
db: AsyncSession = Depends(get_db),
token: str = Depends(oauth2_scheme),
current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))
):
'''Admin/manager can unlock a user account by resetting lock status and login attempts'''
success = await UserService.unlock_user_account(db, user_id)
if not success:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found or not locked")
updated_user = await UserService.get_by_id(db, user_id)
return {
"message": f"User account {user_id} has been unlocked successfully.",
"is_locked": updated_user.is_locked,
"failed_login_attempts": updated_user.failed_login_attempts,
}

@router.get("/users/", response_model=UserListResponse, tags=["User Management Requires (Admin or Manager Roles)"])
async def list_users(
Expand Down Expand Up @@ -233,6 +283,14 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends(), session: Async
return {"access_token": access_token, "token_type": "bearer"}
raise HTTPException(status_code=401, detail="Incorrect email or password.")

# adding new route for registering users
@router.post("/register")
async def register_user_route(
user_data: UserCreate,
db: AsyncSession = Depends(get_db),
email_service: EmailService = Depends(get_email_service),
):
return await UserService.register_user(db, user_data.model_dump(), email_service)

@router.get("/verify-email/{user_id}/{token}", status_code=status.HTTP_200_OK, name="verify_email", tags=["Login and Registration"])
async def verify_email(user_id: UUID, token: str, db: AsyncSession = Depends(get_db), email_service: EmailService = Depends(get_email_service)):
Expand Down
24 changes: 23 additions & 1 deletion app/schemas/user_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re
from app.models.user_model import UserRole
from app.utils.nickname_gen import generate_nickname
from uuid import UUID


def validate_url(url: Optional[str]) -> Optional[str]:
Expand All @@ -17,6 +18,27 @@ def validate_url(url: Optional[str]) -> Optional[str]:
raise ValueError('Invalid URL format')
return url

# adding class. allowed updated fields for public users
class UserUpdatePublic(BaseModel):
first_name: Optional[str] = Field(None, example="John")
last_name: Optional[str] = Field(None, example="Doe")
password: Optional[str] = Field(None, min_length=8)
phone_number: Optional[str] = None
email: Optional[EmailStr] = Field(None, example="john.doe@example.com")
github_profile_url: Optional[str] = Field(None, example="https://github.com/johndoe")
linkedin_profile_url: Optional[str] =Field(None, example="https://linkedin.com/in/johndoe")

class Config:
from_attributes = True

# adding class. allowed updated fields for admins only
class UserUpdateAdmin(UserUpdatePublic):
role: Optional[UserRole] = None
email_verified: Optional[bool] = None
is_locked: Optional[bool] = None
verification_token: Optional[str] = None
is_professional: Optional[bool] = None

class UserBase(BaseModel):
email: EmailStr = Field(..., example="john.doe@example.com")
nickname: Optional[str] = Field(None, min_length=3, pattern=r'^[\w-]+$', example=generate_nickname())
Expand All @@ -26,7 +48,7 @@ class UserBase(BaseModel):
profile_picture_url: Optional[str] = Field(None, example="https://example.com/profiles/john.jpg")
linkedin_profile_url: Optional[str] =Field(None, example="https://linkedin.com/in/johndoe")
github_profile_url: Optional[str] = Field(None, example="https://github.com/johndoe")
role: UserRole
role: Optional[UserRole] = UserRole.AUTHENTICATED

_validate_urls = validator('profile_picture_url', 'linkedin_profile_url', 'github_profile_url', pre=True, allow_reuse=True)(validate_url)

Expand Down
15 changes: 12 additions & 3 deletions app/services/email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from app.models.user_model import User

class EmailService:
def __init__(self, template_manager: TemplateManager):
self.smtp_client = SMTPClient(
def __init__(self, template_manager: TemplateManager, smtp_client: SMTPClient = None):
self.smtp_client = smtp_client or SMTPClient(
server=settings.smtp_server,
port=settings.smtp_port,
username=settings.smtp_username,
Expand All @@ -34,4 +34,13 @@ async def send_verification_email(self, user: User):
"name": user.first_name,
"verification_url": verification_url,
"email": user.email
}, 'email_verification')
}, 'email_verification')

async def send_email(self, to_email: str, subject: str, body: str):
self.smtp_client.send_email(subject, body, to_email)


async def send_professional_status_upgraded_email(self, user: User):
subject = "Professional Status Upgraded."
body = f"Dear {user.first_name or user.nickname},\n\nYour account has been upgraded to professional status.\n\nYour Team"
await self.send_email(user.email, subject, body)
Loading