Skip to content

Conversation

iMercyvlogs
Copy link
Collaborator

Checks the ownership of each category,party and group before saving.

@iMercyvlogs iMercyvlogs requested a review from kofimokome October 8, 2025 23:38
Copy link

sourceant bot commented Oct 8, 2025

Code Review Summary

✨ The Pull Request significantly improves the security posture of the TransactionController by implementing crucial ownership checks for related resources (groups, parties, and categories) during both transaction creation and updates. This ensures that users can only associate transactions with resources they legitimately own. Additionally, the error handling within transaction blocks has been enhanced to correctly propagate HttpExceptions, providing more accurate feedback for validation failures.

🚀 Key Improvements

  • Enhanced Resource Ownership Validation: Crucial security checks are now in place to prevent unauthorized association of groups, parties, and categories with transactions.
  • Improved Error Handling: HttpExceptions thrown during validation within database transactions are now properly caught and re-thrown with their original status codes.

💡 Minor Suggestions

  • The ownership validation logic is duplicated across the store and update methods. Refactoring this into a private helper method would significantly improve code maintainability and reduce redundancy.
  • Consider using Laravel Form Requests to encapsulate validation logic, further decoupling it from the controller methods.

🚨 Critical Issues

  • Bug in Update Method's Category Check: The category ownership validation in the update method incorrectly attempts to use an uninitialized $categories variable, which would bypass this critical security check. The proposed refactoring addresses this by ensuring correct variable initialization.
  • Inconsistent Exception Handling: The update method's catch block does not include the necessary logic to re-throw HttpExceptions, unlike the store method. This means client-side 4xx errors related to ownership validation would be reported as generic 500 errors.

Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

if (isset($data['categories'])) {
$categories = $data['categories'];
unset($data['categories']);
}
Copy link

Choose a reason for hiding this comment

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

Introduce a new private helper method authorizeTransactionOwnership to centralize and reuse the ownership validation logic for groups, parties, and categories. This method will take the validated data and categories array as arguments, improving modularity and preventing code duplication across controller actions.

Suggested change
}
private function authorizeTransactionOwnership(array $data, array $categories = []): void
{
/** @var \App\Models\User $user */
$user = auth()->user();
// check for group ownership
if (isset($data['group_id']) && $data['group_id'] && !$user->groups()->where('id', $data['group_id'])->exists()) {
throw new HttpException(400, 'The selected group does not belong to user');
}
// check for party ownership
if (isset($data['party_id']) && $data['party_id'] && !$user->parties()->where('id', $data['party_id'])->exists()) {
throw new HttpException(400, 'The selected party does not belong to user');
}
// check for categories ownership
if (!empty($categories)) {
$user_category_ids = $user->categories()->pluck('id')->toArray();
$invalid_categories = array_diff($categories, $user_category_ids);
if (!empty($invalid_categories)) {
throw new HttpException(400, 'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
}
}

Comment on lines +266 to +288
$user = auth()->user();

//check for group
if (isset($data['group_id']) && $data['group_id']){
if (!$user->groups()->where('id', $data['group_id'])->exists()){
throw new HttpException(400,'The selected group does not belong to user');
}
}
//check for party
if (isset($data['party_id']) && $data['party_id']) {
if (!$user->parties()->where('id', $data['party_id'])->exists()){
throw new HttpException(400,'The selected party does not belong to user');
}
}

//check for categories
if (!empty($categories)){
$user_category_ids = $user->categories()->pluck('id')->toArray(); //get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); //find any ids not owned by user
if (!empty($invalid_categories)){
throw new HttpException(400,'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
Copy link

Choose a reason for hiding this comment

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

These ownership checks are duplicated in both the store and update methods. Extracting this logic into a private helper method will significantly improve maintainability, reduce redundancy, and make your controller actions cleaner. Consider creating a private function authorizeTransactionOwnership(array $data, array $categories = []): void.

Suggested change
$user = auth()->user();
//check for group
if (isset($data['group_id']) && $data['group_id']){
if (!$user->groups()->where('id', $data['group_id'])->exists()){
throw new HttpException(400,'The selected group does not belong to user');
}
}
//check for party
if (isset($data['party_id']) && $data['party_id']) {
if (!$user->parties()->where('id', $data['party_id'])->exists()){
throw new HttpException(400,'The selected party does not belong to user');
}
}
//check for categories
if (!empty($categories)){
$user_category_ids = $user->categories()->pluck('id')->toArray(); //get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); //find any ids not owned by user
if (!empty($invalid_categories)){
throw new HttpException(400,'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
$this->authorizeTransactionOwnership($data, $categories);

Comment on lines +581 to +604
$user = auth()->user();

//check for group
if (isset($validatedData['group_id']) && $validatedData['group_id']){
if (!$user->groups()->where('id', $validatedData['group_id'])->exists()){
throw new HttpException(400,'The selected group does not belong to user');

}
}
//check for party
if (isset($validatedData['party_id']) && $validatedData['party_id']) {
if (!$user->parties()->where('id', $validatedData['party_id'])->exists()){
throw new HttpException(400,'The selected party does not belong to user');
}
}

//check for categories
if (!empty($categories)){
$user_category_ids = $user->categories()->pluck('id')->toArray(); //get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); //find any ids not owned by user
if (!empty($invalid_categories)){
throw new HttpException(400,'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
Copy link

Choose a reason for hiding this comment

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

This block of ownership checks is duplicated from the store method, indicating a need for refactoring into a shared private method. Additionally, the $categories variable is not defined within the scope of the update method's ownership check, leading to a PHP error if categories are present in the request. The refactored helper method should explicitly accept the categories from $validatedData to correct this bug.

Suggested change
$user = auth()->user();
//check for group
if (isset($validatedData['group_id']) && $validatedData['group_id']){
if (!$user->groups()->where('id', $validatedData['group_id'])->exists()){
throw new HttpException(400,'The selected group does not belong to user');
}
}
//check for party
if (isset($validatedData['party_id']) && $validatedData['party_id']) {
if (!$user->parties()->where('id', $validatedData['party_id'])->exists()){
throw new HttpException(400,'The selected party does not belong to user');
}
}
//check for categories
if (!empty($categories)){
$user_category_ids = $user->categories()->pluck('id')->toArray(); //get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); //find any ids not owned by user
if (!empty($invalid_categories)){
throw new HttpException(400,'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
$this->authorizeTransactionOwnership($validatedData, $validatedData['categories'] ?? []);

Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

Comment on lines 597 to 603
if (! empty($categories)) {
$user_category_ids = $user->categories()->pluck('id')->toArray(); // get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); // find any ids not owned by user
if (! empty($invalid_categories)) {
throw new HttpException(400, 'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
Copy link

Choose a reason for hiding this comment

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

The update method's ownership check for categories uses an undefined $categories variable. This will lead to a PHP Undefined variable error if categories are provided in the request. It should directly use $validatedData['categories'] as store method uses $data['categories'] (after assigning it to a local $categories variable). Making this change simplifies the logic and directly uses the validated input.

Suggested change
if (! empty($categories)) {
$user_category_ids = $user->categories()->pluck('id')->toArray(); // get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); // find any ids not owned by user
if (! empty($invalid_categories)) {
throw new HttpException(400, 'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
if (isset($validatedData['categories']) && ! empty($validatedData['categories'])) {
$user_category_ids = $user->categories()->pluck('id')->toArray();
$invalid_categories = array_diff($validatedData['categories'], $user_category_ids);
if (! empty($invalid_categories)) {
throw new HttpException(400, 'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}

Copy link
Collaborator

@kofimokome kofimokome left a comment

Choose a reason for hiding this comment

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

We use $this->success() and $this->failure() for success and error messages respectively. Do not throw and HttpException except you want to write a try-catch to catch the exception and use the functions above to return the appriate message to the user.

@nfebe
Copy link
Contributor

nfebe commented Oct 9, 2025

@kofimokome Someone was already working on this ?

@kofimokome
Copy link
Collaborator

kofimokome commented Oct 10, 2025

Yes, I told her to work on it so that she gets familiar with the code. Also the guy hasn't responded to our feedback. If Mercy's work solves the issue and the other guy takes forever to reply, I would merge hers. For now I will leave both open.

@iMercyvlogs split this into two pull requests. One for #99 and the other for #100.

@iMercyvlogs
Copy link
Collaborator Author

iMercyvlogs commented Oct 10, 2025 via email

@kofimokome
Copy link
Collaborator

Split it, I will merge the other pull request first if it's okay

@iMercyvlogs iMercyvlogs force-pushed the feat/userCheckPerRequest branch from 2659af5 to d3f93c3 Compare October 12, 2025 19:13
Copy link

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

// If an HttpException was thrown earlier (e.g., from ownership check), re-throw it with the correct status code.
if ($e instanceof HttpException) {
return $this->failure($e->getMessage(), $e->getStatusCode());
}
Copy link

Choose a reason for hiding this comment

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

The update method's catch block does not correctly handle HttpExceptions thrown by the newly added ownership checks. This means that a user-facing 400 error would be converted into a generic 500 error, obscuring the actual problem. This change aligns the error handling with the store method, ensuring appropriate HTTP status codes are returned.

Suggested change
}
// If an HttpException was thrown earlier (e.g., from ownership check), re-throw it with the correct status code.
if ($e instanceof HttpException) {
return $this->failure($e->getMessage(), $e->getStatusCode());
}
return $this->failure('Failed to update transaction', 500, [$e->getMessage()]);

Comment on lines +265 to +288
//ownership checks for authenticated user
$user = auth()->user();

//check for group
if (isset($data['group_id']) && $data['group_id']){
if (!$user->groups()->where('id', $data['group_id'])->exists()){
throw new HttpException(400,'The selected group does not belong to user');
}
}
//check for party
if (isset($data['party_id']) && $data['party_id']) {
if (!$user->parties()->where('id', $data['party_id'])->exists()){
throw new HttpException(400,'The selected party does not belong to user');
}
}

//check for categories
if (!empty($categories)){
$user_category_ids = $user->categories()->pluck('id')->toArray(); //get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); //find any ids not owned by user
if (!empty($invalid_categories)){
throw new HttpException(400,'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
Copy link

Choose a reason for hiding this comment

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

The ownership checks for groups, parties, and categories are duplicated across the store and update methods. This is a prime candidate for refactoring. Extracting this logic into a private helper method will improve maintainability, reduce redundancy, and make the code cleaner. This suggestion replaces the duplicated block with a call to the new validateResourceOwnership method.

Suggested change
//ownership checks for authenticated user
$user = auth()->user();
//check for group
if (isset($data['group_id']) && $data['group_id']){
if (!$user->groups()->where('id', $data['group_id'])->exists()){
throw new HttpException(400,'The selected group does not belong to user');
}
}
//check for party
if (isset($data['party_id']) && $data['party_id']) {
if (!$user->parties()->where('id', $data['party_id'])->exists()){
throw new HttpException(400,'The selected party does not belong to user');
}
}
//check for categories
if (!empty($categories)){
$user_category_ids = $user->categories()->pluck('id')->toArray(); //get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); //find any ids not owned by user
if (!empty($invalid_categories)){
throw new HttpException(400,'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
$user = auth()->user();
$this->validateResourceOwnership($user, $data, $categories);

Comment on lines +581 to +604
$user = auth()->user();

//check for group
if (isset($validatedData['group_id']) && $validatedData['group_id']){
if (!$user->groups()->where('id', $validatedData['group_id'])->exists()){
throw new HttpException(400,'The selected group does not belong to user');

}
}
//check for party
if (isset($validatedData['party_id']) && $validatedData['party_id']) {
if (!$user->parties()->where('id', $validatedData['party_id'])->exists()){
throw new HttpException(400,'The selected party does not belong to user');
}
}

//check for categories
if (!empty($categories)){
$user_category_ids = $user->categories()->pluck('id')->toArray(); //get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); //find any ids not owned by user
if (!empty($invalid_categories)){
throw new HttpException(400,'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
Copy link

Choose a reason for hiding this comment

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

Similar to the store method, the ownership checks here are duplicated logic. Refactor this into a private helper method for better maintainability. Additionally, the original implementation of this block would have led to a bug where category ownership checks are skipped due to the $categories variable being uninitialized in this scope. The suggested code properly initializes $inputCategories for the check.

Suggested change
$user = auth()->user();
//check for group
if (isset($validatedData['group_id']) && $validatedData['group_id']){
if (!$user->groups()->where('id', $validatedData['group_id'])->exists()){
throw new HttpException(400,'The selected group does not belong to user');
}
}
//check for party
if (isset($validatedData['party_id']) && $validatedData['party_id']) {
if (!$user->parties()->where('id', $validatedData['party_id'])->exists()){
throw new HttpException(400,'The selected party does not belong to user');
}
}
//check for categories
if (!empty($categories)){
$user_category_ids = $user->categories()->pluck('id')->toArray(); //get user category ids
$invalid_categories = array_diff($categories, $user_category_ids); //find any ids not owned by user
if (!empty($invalid_categories)){
throw new HttpException(400,'Some of the selected categories do not belong to user. Invalid category IDs: '.implode(',', $invalid_categories));
}
}
$user = auth()->user();
$inputCategories = $validatedData['categories'] ?? [];
$this->validateResourceOwnership($user, $validatedData, $inputCategories);

@nfebe
Copy link
Contributor

nfebe commented Oct 15, 2025

@nfebe nfebe closed this Oct 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants