session()->put(self::CLAIMING, $organization->id); return Inertia::location($this->github()->scopes(['read:org'])->redirect()->getTargetUrl()); } /** * GitHub only sends back to a path under the callback registered for sign-in, so this is where it lands. */ public function store(Request $request, #[CurrentUser] User $user, GitHub $github, CreateOrganizationAdmin $action): RedirectResponse { $organization = Profile::query() ->where('type', ProfileType::Organization) ->find($request->session()->pull(self::CLAIMING)); if (! $organization instanceof Profile) { Inertia::flash('toast', ['type' => 'error', 'message' => __('Unable to claim this organization. Please try again.')]); return to_route('log.show', $user->log); } $page = to_route('user.show', $organization->handle); if ($request->has('error')) { return $this->failed(__('GitHub was not asked about your organizations, so the page was not claimed.'), $page); } try { $githubUser = $this->github()->user(); } catch (InvalidStateException) { return $this->failed(__('Unable to claim this organization. Please try again.'), $page); } assert($githubUser instanceof GitHubUser); if ((string) $githubUser->getId() !== $user->github_id) { return $this->failed(__('GitHub is signed in as @:login. Sign in there as @:handle to claim this page.', [ 'login' => $githubUser->getNickname(), 'handle' => $user->profile->handle, ]), $page); } $handle = $organization->handle; // An organization restricting OAuth apps hides its members from those its owners haven't approved, which they // do from the app's page in their settings, where members request it. $failure = match ($github->organizationMembership($githubUser->token, $handle, $organization->github_id)) { OrganizationMembership::Member => null, OrganizationMembership::NotMember => [__('GitHub says you are not a member of @:handle.', ['handle' => $handle]), null], OrganizationMembership::Restricted => [ __('@:handle only shares its members with apps its owners approved. On GitHub, grant /dev/null access if you are an owner, or request it from them, then try again.', ['handle' => $handle]), 'https://github.com/settings/connections/applications/'.config()->string('services.github.client_id'), ], OrganizationMembership::SingleSignOn => [ __('@:handle signs its members in with single sign-on. Sign in to @:handle on GitHub, then try again.', ['handle' => $handle]), "https://github.com/orgs/{$handle}/sso", ], }; if ($failure !== null) { return $this->failed($failure[0], $page, $failure[1]); } $action->handle($organization, $user); Inertia::flash('toast', ['type' => 'success', 'message' => __('You are now an admin of @:handle.', ['handle' => $organization->handle])]); return $page; } /** * GitHub sends the user back here rather than to sign-in, and checks the token is redeemed for the same URL. */ private function github(): GithubProvider { $provider = Socialite::driver('github'); assert($provider instanceof GithubProvider); return $provider->redirectUrl(route('organization-admin.store')); } /** * The page says why the claim failed until the user leaves it, with where to approve /dev/null when that's why. */ private function failed(string $message, RedirectResponse $redirect, ?string $approvalUrl = null): RedirectResponse { Inertia::flash('claimFailed', ['message' => $message, 'approvalUrl' => $approvalUrl]); return $redirect; } }