Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
public class UpdateProfileHandler(
UserManager<ApplicationUser> userManager,
IFileStorageService fileStorageService,
ILogger<UpdateProfileHandler> logger) : IRequestHandler<UpdateProfileCommand, ServiceResponse<string>>
ILogger<UpdateProfileHandler> logger) : IRequestHandler<UpdateProfileCommand, ServiceResponse<UserDto>>
{
public async Task<ServiceResponse<string>> Handle(UpdateProfileCommand request, CancellationToken cancellationToken)
public async Task<ServiceResponse<UserDto>> Handle(UpdateProfileCommand request, CancellationToken cancellationToken)
{
try
{
var user = await userManager.FindByIdAsync(request.UserId.ToString());
if (user is null) return new ServiceResponse<string>(false, "User not found");
if (user is null) return new ServiceResponse<UserDto>(false, "User not found");

if (!string.IsNullOrEmpty(request.FirstName))
user.FirstName = request.FirstName;
Expand Down Expand Up @@ -60,15 +60,42 @@
var errors = result.Errors.Select(e => e.Description).ToList();
logger.LogWarning("Failed to update profile for user {UserId}: {Errors}", user.Id,
string.Join(", ", errors));
return new ServiceResponse<string>(false, "Failed to update profile");
return new ServiceResponse<UserDto>(false, "Failed to update profile");
}

var roles = await userManager.GetRolesAsync(user);
string? profilePicUrl = null;
if (!string.IsNullOrWhiteSpace(user.ProfilePictureUrl))
{
try
{
profilePicUrl = await fileStorageService.GetFileUrlAsync(user.ProfilePictureUrl);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to resolve profile picture URL for user {UserId}. Key: {Key}", user.Id,
user.ProfilePictureUrl);
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
Comment on lines +74 to +78
}
Comment on lines +67 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Profile picture URL resolution failure returns incomplete data after successful upload.

If a new profile picture is uploaded successfully (lines 45-52) and stored in the database (line 57), but GetFileUrlAsync fails when building the response (line 72), the returned UserDto will have a null profilePicUrl even though the upload succeeded. This misleads the client into believing the upload failed when it actually succeeded.

Consider one of these approaches:

  1. Call GetFileUrlAsync immediately after SaveFileAsync (line 50) to validate the URL is retrievable before saving to the database.
  2. Return the storage key instead of the resolved URL, letting clients resolve URLs on subsequent requests.
  3. Document that a null profilePicUrl in the response doesn't necessarily mean upload failed, but this is poor UX.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs`
around lines 67 - 79, The response currently resolves the profile URL later and
can return null even after a successful upload; modify UpdateProfileHandler to
call fileStorageService.GetFileUrlAsync immediately after
fileStorageService.SaveFileAsync (use SaveFileAsync and GetFileUrlAsync) when a
new picture is uploaded, capture the resolved URL and use that resolved value
when constructing the UserDto (while still persisting the storage key in
user.ProfilePictureUrl), and if GetFileUrlAsync fails after SaveFileAsync log
the error and include the storage key as a fallback in the response so clients
are not misled by a null profilePicUrl.


var userDto = new UserDto(
user.Id,
user.Email ?? string.Empty,
user.FirstName,
user.LastName,
user.IsActive,
user.CreatedAt,
roles.ToList(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Unnecessary list conversion.

GetRolesAsync already returns IList<string>. The ToList() call creates a redundant copy.

♻️ Proposed refactor
-            roles.ToList(),
+            roles,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
roles.ToList(),
roles,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/Src/PIED_LMS.Application/UserCases/Commands/Identity/UpdateProfileHandler.cs`
at line 88, The code passes roles.ToList() to the update call but GetRolesAsync
already returns an IList<string>, so remove the redundant allocation and pass
the existing roles collection directly; locate the roles variable (result of
UserManager.GetRolesAsync) in UpdateProfileHandler (Handle method) and replace
the roles.ToList() usage with roles.

user.Bio,
profilePicUrl
);

return new ServiceResponse<string>(true, "Profile updated successfully");
return new ServiceResponse<UserDto>(true, "Profile updated successfully", userDto);
}
catch (Exception ex)
{
logger.LogError(ex, "Error occurred while updating profile for user {UserId}", request.UserId);
return new ServiceResponse<string>(false, "An error occurred while updating the profile");
return new ServiceResponse<UserDto>(false, "An error occurred while updating the profile");
}
}
}
2 changes: 1 addition & 1 deletion backend/Src/PIED_LMS.Contract/Services/Identity/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public record UpdateProfileCommand(
string? LastName,
string? Bio,
IFormFile? ProfilePicture
) : IRequest<ServiceResponse<string>>;
) : IRequest<ServiceResponse<UserDto>>;

public sealed record UpdateProfileRequest
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void AddRoutes(IEndpointRouteBuilder app)
.WithName("UpdateProfile")
.RequireAuthorization()
.DisableAntiforgery()
.WithServiceResponseOpenApi<string>(ServiceResponseStatusProfile.OkOrBadRequest);
.WithServiceResponseOpenApi<UserDto>(ServiceResponseStatusProfile.OkOrBadRequest);

group.MapGet("/me", GetMe)
.WithName("GetMe")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ public static IResult ToActionResult<T>(this ServiceResponse<T> result, HttpCont
{
if (result.Success)
{
if (result.Data is null && typeof(T).IsClass)
return Results.NotFound(result);

return Results.Ok(result);
}

Expand All @@ -25,7 +22,7 @@ public static IResult ToActionResult<T>(this ServiceResponse<T> result, HttpCont
return Results.Json(result, statusCode: StatusCodes.Status401Unauthorized);

if (result.ErrorCode == "FORBIDDEN" || result.ErrorCode == "ACCESS_DENIED" ||
(result.Message.Contains("authorized") || result.Message.Contains("permission")))
(result.Message is not null && (result.Message.Contains("authorized") || result.Message.Contains("permission"))))
return Results.Json(result, statusCode: StatusCodes.Status403Forbidden);

return result.ErrorCode switch
Expand Down
Loading