-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Implement organization hierarchy and multi-tenancy system #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } |
| 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(); | ||
| } | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -20Repository: 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 -100Repository: 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 -5Repository: 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.phpRepository: 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 -150Repository: 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 phpRepository: 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 -10Repository: 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 phpRepository: 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 phpRepository: 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 -100Repository: 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.phpRepository: 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 5Repository: johnproblems/topgun Length of output: 45 Inconsistent handling of The Although Add - $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.
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
There was a problem hiding this comment.
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
getUserOrganizationsmethod declares a return type of\Illuminate\Database\Eloquent\Collectionbut returnscollect()when there's no authenticated user. Thecollect()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.