-
Notifications
You must be signed in to change notification settings - Fork 62
feat: add multi-organization support #275
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
7daa933
a4c45cf
4a0b4ba
a24e091
4d5ae23
e65d46a
2f323a7
83a2be5
ebdf7ed
f01dbb1
35369b8
2dcb3b3
668276b
91dfcca
7903440
4aa4687
9b54e01
57ed234
8a3a85b
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,66 @@ | ||
| <?php | ||
|
|
||
| namespace App\Http\Middleware; | ||
|
|
||
| use App\Models\Agent; | ||
| use App\Models\User; | ||
| use App\Services\CurrentOrganization; | ||
| use Closure; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Auth; | ||
| use Illuminate\Support\Facades\Session; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
|
|
||
| class SetCurrentOrganization | ||
| { | ||
| public function __construct( | ||
| private readonly CurrentOrganization $currentOrganization | ||
| ) {} | ||
|
|
||
| public function handle(Request $request, Closure $next): Response | ||
| { | ||
| /** @var User|Agent|null $authenticatable */ | ||
| $authenticatable = $request->user(); | ||
|
|
||
| if ($authenticatable instanceof Agent) { | ||
| $this->currentOrganization->set($authenticatable->organization); | ||
| } elseif ($authenticatable instanceof User) { | ||
| $this->currentOrganization->reset(); | ||
|
|
||
| if ($request->is('api/*')) { | ||
| $this->resolveApiOrganization($request, $authenticatable); | ||
| } else { | ||
| /** @var string|null $cookieOrgId */ | ||
| $cookieOrgId = $request->cookie(CurrentOrganization::COOKIE_NAME); | ||
| $this->currentOrganization->resolveForUser($authenticatable, $cookieOrgId); | ||
| } | ||
|
|
||
| if (! $this->currentOrganization->isResolved()) { | ||
| Auth::guard('web')->logout(); | ||
| Session::invalidate(); | ||
| Session::regenerateToken(); | ||
| session()->flash('error', __('Your account is not a member of any organization. Please contact an administrator.')); | ||
|
|
||
| return redirect()->route('login'); | ||
| } | ||
| } | ||
|
|
||
| return $next($request); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve organization for API requests. | ||
| * Aborts with 403 when the client explicitly requests an org that is invalid or inaccessible. | ||
| */ | ||
| private function resolveApiOrganization(Request $request, User $user): void | ||
| { | ||
| /** @var string|null $orgId */ | ||
| $orgId = $request->query('org_id') ?? $request->header('X-Organization-Id'); | ||
|
|
||
| $this->currentOrganization->resolveForUser($user, $orgId); | ||
|
|
||
| if ($orgId && (! $this->currentOrganization->isResolved() || $this->currentOrganization->id() !== $orgId)) { | ||
| abort(403, 'The requested organization is not accessible.'); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| <?php | ||
|
|
||
| namespace App\Livewire\Configuration; | ||
|
|
||
| use App\Models\Organization as OrganizationModel; | ||
| use App\Models\Scopes\OrganizationScope; | ||
| use App\Traits\Toast; | ||
| use Illuminate\Contracts\View\View; | ||
| use Illuminate\Database\Eloquent\Collection; | ||
| use Illuminate\Foundation\Auth\Access\AuthorizesRequests; | ||
| use Livewire\Attributes\Computed; | ||
| use Livewire\Attributes\Title; | ||
| use Livewire\Component; | ||
|
|
||
| #[Title('Configuration')] | ||
| class Organization extends Component | ||
| { | ||
| use AuthorizesRequests; | ||
| use Toast; | ||
|
|
||
| public bool $showCreateModal = false; | ||
|
|
||
| public string $newOrgName = ''; | ||
|
|
||
| public bool $showEditModal = false; | ||
|
|
||
| public ?string $editingOrgId = null; | ||
|
|
||
| public string $editOrgName = ''; | ||
|
|
||
| public bool $showDeleteModal = false; | ||
|
|
||
| public ?string $deleteOrgId = null; | ||
|
|
||
| public function mount(): void | ||
| { | ||
| $this->authorize('viewAny', OrganizationModel::class); | ||
| } | ||
|
|
||
| /** | ||
| * @return Collection<int, OrganizationModel> | ||
| */ | ||
| #[Computed] | ||
| public function organizations(): Collection | ||
| { | ||
| return OrganizationModel::withCount([ | ||
| 'users', | ||
| 'databaseServers' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| 'volumes' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| 'agents' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| ]) | ||
| ->orderByDesc('is_main') | ||
| ->orderBy('name') | ||
| ->get(); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| public function openCreateModal(): void | ||
| { | ||
| $this->newOrgName = ''; | ||
| $this->showCreateModal = true; | ||
| } | ||
|
Comment on lines
+57
to
+61
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: # Find and read the Organization.php Livewire component
find . -path "*/app/Livewire/Configuration/Organization.php" -type f | head -1Repository: David-Crty/databasement Length of output: 113 🏁 Script executed: # Get the file size first to determine how to read it
wc -l app/Livewire/Configuration/Organization.phpRepository: David-Crty/databasement Length of output: 115 🏁 Script executed: # Read the full file since we need to understand the context
cat -n app/Livewire/Configuration/Organization.phpRepository: David-Crty/databasement Length of output: 6122 🏁 Script executed: # Search for validation patterns in Livewire components
rg "resetValidation" --type php -C 2Repository: David-Crty/databasement Length of output: 740 🏁 Script executed: # Check if this is a Laravel/Livewire project by looking for config or composer
ls -la | grep -E "composer|artisan|laravel"Repository: David-Crty/databasement Length of output: 259 Reset the validation error bag when opening modals to prevent stale errors from previous submissions. The 🩹 Small fix public function openCreateModal(): void
{
+ $this->resetValidation();
$this->newOrgName = '';
$this->showCreateModal = true;
}
public function openEditModal(string $orgId): void
{
+ $this->resetValidation();
$org = OrganizationModel::findOrFail($orgId);This pattern is already established elsewhere in the codebase (ConfigurationForm.php, NotificationChannelForm.php use 🤖 Prompt for AI Agents |
||
|
|
||
| public function createOrganization(): mixed | ||
| { | ||
| $this->authorize('create', OrganizationModel::class); | ||
|
|
||
| $this->validate([ | ||
| 'newOrgName' => 'required|string|max:255|unique:organizations,name', | ||
| ]); | ||
|
|
||
| OrganizationModel::create([ | ||
| 'name' => $this->newOrgName, | ||
| ]); | ||
|
|
||
| $this->showCreateModal = false; | ||
| $this->newOrgName = ''; | ||
|
|
||
| $this->success(__('Organization created.')); | ||
|
|
||
| return $this->redirect(route('configuration.organizations'), navigate: true); | ||
| } | ||
|
|
||
| public function openEditModal(string $orgId): void | ||
| { | ||
| $org = OrganizationModel::findOrFail($orgId); | ||
|
|
||
| $this->authorize('update', $org); | ||
|
|
||
| $this->editingOrgId = $orgId; | ||
| $this->editOrgName = $org->name; | ||
| $this->showEditModal = true; | ||
| } | ||
|
|
||
| public function updateOrganization(): mixed | ||
| { | ||
| $org = OrganizationModel::findOrFail($this->editingOrgId); | ||
|
|
||
| $this->authorize('update', $org); | ||
|
|
||
| $this->validate([ | ||
| 'editOrgName' => 'required|string|max:255|unique:organizations,name,'.$org->id, | ||
| ]); | ||
|
|
||
| $org->update(['name' => $this->editOrgName]); | ||
|
|
||
| $this->showEditModal = false; | ||
| $this->editingOrgId = null; | ||
|
|
||
| $this->success(__('Organization updated.')); | ||
|
|
||
| return $this->redirect(route('configuration.organizations'), navigate: true); | ||
| } | ||
|
|
||
| public function confirmDelete(string $orgId): void | ||
| { | ||
| $org = OrganizationModel::withCount([ | ||
| 'databaseServers' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| 'volumes' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| 'agents' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| ])->findOrFail($orgId); | ||
|
|
||
| $this->authorize('delete', $org); | ||
|
|
||
| $this->deleteOrgId = $orgId; | ||
| $this->showDeleteModal = true; | ||
| } | ||
|
|
||
| public function deleteOrganization(): mixed | ||
| { | ||
| $org = OrganizationModel::withCount([ | ||
| 'databaseServers' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| 'volumes' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| 'agents' => fn ($q) => $q->withoutGlobalScope(OrganizationScope::class), | ||
| ])->findOrFail($this->deleteOrgId); | ||
|
|
||
| $this->authorize('delete', $org); | ||
|
|
||
| $org->delete(); | ||
|
|
||
| $this->showDeleteModal = false; | ||
| $this->deleteOrgId = null; | ||
|
|
||
| $this->success(__('Organization deleted.')); | ||
|
|
||
| return $this->redirect(route('configuration.organizations'), navigate: true); | ||
| } | ||
|
|
||
| public function render(): View | ||
| { | ||
| return view('livewire.configuration.organization', [ | ||
| 'organizations' => $this->organizations(), | ||
| 'headers' => [ | ||
| ['key' => 'name', 'label' => __('Name')], | ||
| ['key' => 'id', 'label' => __('ID')], | ||
| ['key' => 'users_count', 'label' => __('Users')], | ||
| ['key' => 'database_servers_count', 'label' => __('Servers')], | ||
| ['key' => 'volumes_count', 'label' => __('Volumes')], | ||
| ['key' => 'agents_count', 'label' => __('Agents')], | ||
| ['key' => 'actions', 'label' => '', 'class' => 'w-32'], | ||
| ], | ||
| ]); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.