Skip to content
Open
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
70 changes: 70 additions & 0 deletions app/Contracts/OrganizationServiceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace App\Contracts;

use App\Models\Organization;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;

interface OrganizationServiceInterface
{
/**
* Create a new organization with proper hierarchy validation
*/
public function createOrganization(array $data, ?Organization $parent = null): Organization;

/**
* Update organization with validation
*/
public function updateOrganization(Organization $organization, array $data): Organization;

/**
* Attach a user to an organization with a specific role
*/
public function attachUserToOrganization(Organization $organization, User $user, string $role, array $permissions = []): void;

/**
* Update user's role and permissions in an organization
*/
public function updateUserRole(Organization $organization, User $user, string $role, array $permissions = []): void;

/**
* Remove user from organization
*/
public function detachUserFromOrganization(Organization $organization, User $user): void;

/**
* Switch user's current organization context
*/
public function switchUserOrganization(User $user, Organization $organization): void;

/**
* Get organizations accessible by a user
*/
public function getUserOrganizations(User $user): Collection;

/**
* Check if user can perform an action on a resource within an organization
*/
public function canUserPerformAction(User $user, Organization $organization, string $action, $resource = null): bool;

/**
* Get organization hierarchy tree
*/
public function getOrganizationHierarchy(Organization $rootOrganization): array;

/**
* Move organization to a new parent (with validation)
*/
public function moveOrganization(Organization $organization, ?Organization $newParent): Organization;

/**
* Delete organization with proper cleanup
*/
public function deleteOrganization(Organization $organization, bool $force = false): bool;

/**
* Get organization usage statistics
*/
public function getOrganizationUsage(Organization $organization): array;
}
210 changes: 210 additions & 0 deletions app/Helpers/OrganizationContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php

namespace App\Helpers;

use App\Models\Organization;
use App\Models\User;
use Illuminate\Support\Facades\Auth;

class OrganizationContext
{
/**
* Get the current organization for the authenticated user
*/
public static function current(): ?Organization
{
$user = Auth::user();

return $user?->currentOrganization;
}

/**
* Get the current organization ID for the authenticated user
*/
public static function currentId(): ?string
{
return static::current()?->id;
}

/**
* Check if the current user can perform an action in their current organization
*/
public static function can(string $action, $resource = null): bool
{
$user = Auth::user();
$organization = static::current();

if (! $user || ! $organization) {
return false;
}

return app(\App\Contracts\OrganizationServiceInterface::class)
->canUserPerformAction($user, $organization, $action, $resource);
}

/**
* Check if the current organization has a specific feature
*/
public static function hasFeature(string $feature): bool
{
return static::current()?->hasFeature($feature) ?? false;
}

/**
* Get usage metrics for the current organization
*/
public static function getUsage(): array
{
$organization = static::current();

if (! $organization) {
return [];
}

return app(\App\Contracts\OrganizationServiceInterface::class)
->getOrganizationUsage($organization);
}

/**
* Check if the current organization is within its limits
*/
public static function isWithinLimits(): bool
{
return static::current()?->isWithinLimits() ?? false;
}

/**
* Get the hierarchy type of the current organization
*/
public static function getHierarchyType(): ?string
{
return static::current()?->hierarchy_type;
}

/**
* Check if the current organization is of a specific hierarchy type
*/
public static function isHierarchyType(string $type): bool
{
return static::getHierarchyType() === $type;
}

/**
* Get all organizations accessible by the current user
*/
public static function getUserOrganizations(): \Illuminate\Database\Eloquent\Collection
{
$user = Auth::user();

if (! $user) {
return collect();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Method returns wrong Collection type when no user

The getUserOrganizations method declares a return type of \Illuminate\Database\Eloquent\Collection but returns collect() when there's no authenticated user. The collect() helper returns \Illuminate\Support\Collection, not an Eloquent Collection. This type mismatch can cause issues when callers attempt to use Eloquent-specific methods on the returned collection.

Fix in Cursor Fix in Web


return app(\App\Contracts\OrganizationServiceInterface::class)
->getUserOrganizations($user);
}

/**
* Switch to a different organization
*/
public static function switchTo(Organization $organization): bool
{
$user = Auth::user();

if (! $user) {
return false;
}

try {
app(\App\Contracts\OrganizationServiceInterface::class)
->switchUserOrganization($user, $organization);

return true;
} catch (\Exception $e) {
return false;
}
}

/**
* Get the organization hierarchy starting from the current organization
*/
public static function getHierarchy(): array
{
$organization = static::current();

if (! $organization) {
return [];
}

return app(\App\Contracts\OrganizationServiceInterface::class)
->getOrganizationHierarchy($organization);
}

/**
* Check if the current user is an owner of the current organization
*/
public static function isOwner(): bool
{
$user = Auth::user();
$organization = static::current();

if (! $user || ! $organization) {
return false;
}

$userOrg = $organization->users()->where('user_id', $user->id)->first();

return $userOrg && $userOrg->pivot->role === 'owner';
}

/**
* Check if the current user is an admin of the current organization
*/
public static function isAdmin(): bool
{
$user = Auth::user();
$organization = static::current();

if (! $user || ! $organization) {
return false;
}

$userOrg = $organization->users()->where('user_id', $user->id)->first();

return $userOrg && in_array($userOrg->pivot->role, ['owner', 'admin']);
}

/**
* Get the current user's role in the current organization
*/
public static function getUserRole(): ?string
{
$user = Auth::user();
$organization = static::current();

if (! $user || ! $organization) {
return null;
}

$userOrg = $organization->users()->where('user_id', $user->id)->first();

return $userOrg?->pivot->role;
}

/**
* Get the current user's permissions in the current organization
*/
public static function getUserPermissions(): array
{
$user = Auth::user();
$organization = static::current();

if (! $user || ! $organization) {
return [];
}

$userOrg = $organization->users()->where('user_id', $user->id)->first();

return $userOrg?->pivot->permissions ?? [];
Comment on lines +146 to +208
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find migration files and model definitions related to organization_users
find . -type f -name "*migration*" -o -name "*Organization*" -o -name "*User*" | grep -E "\.(php|js)$" | head -20

Repository: johnproblems/topgun

Length of output: 947


🏁 Script executed:

#!/bin/bash
# Search for organization_users table definition or migrations
rg "organization_users" -A 5 -B 5 --type php | head -100

Repository: johnproblems/topgun

Length of output: 3218


🏁 Script executed:

#!/bin/bash
# Check the Organization model for relationship definition
fd -e php | xargs grep -l "class Organization" | head -5

Repository: johnproblems/topgun

Length of output: 260


🏁 Script executed:

#!/bin/bash
# Read the full migration file
cat database/migrations/2025_08_26_225351_create_organization_users_table.php

Repository: johnproblems/topgun

Length of output: 1213


🏁 Script executed:

#!/bin/bash
# Search for is_active usage in the codebase
rg "is_active" -A 3 -B 3 --type php | head -150

Repository: johnproblems/topgun

Length of output: 9913


🏁 Script executed:

#!/bin/bash
# Check the current OrganizationContext helpers specifically
cat -n app/Helpers/OrganizationContext.php | sed -n '146,208p'

Repository: johnproblems/topgun

Length of output: 2162


🏁 Script executed:

#!/bin/bash
# Search for usage of OrganizationContext helpers
rg "OrganizationContext::(isOwner|isAdmin|getUserRole|getUserPermissions)" -A 2 -B 2 --type php

Repository: johnproblems/topgun

Length of output: 1403


🏁 Script executed:

#!/bin/bash
# Search for tests related to OrganizationContext or membership
fd -e php | xargs grep -l "test.*OrganizationContext\|test.*isOwner\|test.*isAdmin" 2>/dev/null | head -10

Repository: johnproblems/topgun

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for canUserPerformAction in OrganizationService to see how it differs
rg "canUserPerformAction" -A 10 -B 2 --type php

Repository: johnproblems/topgun

Length of output: 5354


🏁 Script executed:

#!/bin/bash
# Search for where is_active is set to false
rg "is_active.*false|is_active\s*=\s*false" -B 5 -A 2 --type php

Repository: johnproblems/topgun

Length of output: 515


🏁 Script executed:

#!/bin/bash
# Search for any removal/disable logic on organization membership
rg "removeUser|disableUser|deactivate.*user|soft.*delete" -i -A 5 --type php | head -100

Repository: johnproblems/topgun

Length of output: 8721


🏁 Script executed:

#!/bin/bash
# Check Organization model canUserPerformAction more closely
rg "canUserPerformAction" -B 5 -A 20 app/Models/Organization.php

Repository: johnproblems/topgun

Length of output: 773


🏁 Script executed:

#!/bin/bash
# Check for any comments or TODOs about is_active usage
rg "is_active" -B 3 -A 3 --type php | rg -i "todo|fixme|disable|deactivate|revoke|suspend" -B 5 -A 5

Repository: johnproblems/topgun

Length of output: 45


Inconsistent handling of is_active pivot flag across permission checks.

The organization_users pivot includes an is_active flag (defined in the migration with default(true)), which is already checked in OrganizationService::canUserPerformAction(). However, the membership helpers in OrganizationContext—isOwner(), isAdmin(), getUserRole(), and getUserPermissions()—do not check this flag. This creates an inconsistency: OrganizationService respects is_active while OrganizationContext does not.

Although is_active is not currently set to false anywhere, this inconsistency is a maintenance risk. If is_active is used to soft-disable memberships (e.g., for revocation or suspension), these helpers would still expose the role and permissions of disabled users, undermining the disable mechanism.

Add .wherePivot('is_active', true) to the membership lookup in each helper:

-        $userOrg = $organization->users()->where('user_id', $user->id)->first();
+        $userOrg = $organization->users()
+            ->where('user_id', $user->id)
+            ->wherePivot('is_active', true)
+            ->first();

Optionally, extract this lookup into a small private/static helper to avoid repetition across all four methods.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/Helpers/OrganizationContext.php around lines 146 to 208, the membership
lookups in isOwner(), isAdmin(), getUserRole(), and getUserPermissions() do not
respect the pivot is_active flag; update each lookup to include
->wherePivot('is_active', true) so only active memberships are considered, and
to avoid repetition extract the membership query into a single private/static
helper (e.g., getUserOrganizationPivot() that returns the userOrg pivot or null)
and have all four methods call that helper.

}
}
Loading