ensurePlatformEnabled(); $workspace = $request->user()->currentWorkspace; if (! $workspace) { return redirect()->route('app.workspaces.create'); } $this->authorize('manageAccounts', $workspace); session([ 'social_connect_workspace' => $workspace->id, 'linkedin_page_reconnect_id' => null, 'social_connect_onboarding' => $request->boolean('onboarding'), ]); return Inertia::location( Socialite::driver($this->driver) ->scopes(config('trypost.platforms.linkedin-page.scopes')) ->with([ 'redirect_uri' => config('services.linkedin-openid.redirect_page'), ]) ->redirect() ->getTargetUrl() ); } public function callback(Request $request): View|RedirectResponse { $workspaceId = session('social_connect_workspace'); if (! $workspaceId) { return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { $socialUser = Socialite::driver($this->driver) ->scopes(config('trypost.platforms.linkedin-page.scopes')) ->with([ 'redirect_uri' => config('services.linkedin-openid.redirect_page'), ]) ->user(); // Fetch organizations the user is admin of $organizations = $this->fetchOrganizations($socialUser->token); if (empty($organizations)) { return $this->popupCallback(false, __('accounts.popup_callback.not_linkedin_admin'), $this->platform->value); } // Store data in session and redirect to selection page session([ 'linkedin_page_pending' => [ 'workspace_id' => $workspace->id, 'user_id' => $socialUser->getId(), 'name' => $socialUser->getName(), 'avatar' => $socialUser->getAvatar(), 'token' => $socialUser->token, 'refresh_token' => $socialUser->refreshToken, 'expires_in' => $socialUser->expiresIn, 'approved_scopes' => $socialUser->approvedScopes ?? [], 'organizations' => $organizations, 'reconnect_id' => session('linkedin_page_reconnect_id'), ], ]); return redirect()->route('app.social.linkedin-page.select-page'); } catch (\Exception $e) { Log::error('LinkedIn Page OAuth Error', [ 'error' => $e->getMessage(), ]); return $this->popupCallback(false, __('accounts.popup_callback.error_connecting'), $this->platform->value); } } public function selectPage(Request $request): Response|RedirectResponse { $pendingData = session('linkedin_page_pending'); if (! $pendingData) { session()->flash('flash.banner', __('accounts.flash.session_expired')); session()->flash('flash.bannerStyle', 'danger'); return redirect()->route('app.accounts'); } $workspace = Workspace::find($pendingData['workspace_id']); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { session()->flash('flash.banner', __('accounts.flash.workspace_not_found')); session()->flash('flash.bannerStyle', 'danger'); return redirect()->route('app.accounts'); } return Inertia::render('accounts/LinkedInPageSelect', [ 'workspace' => $workspace, 'organizations' => $pendingData['organizations'], ]); } public function select(Request $request): View { $request->validate([ 'organization_id' => 'required', 'organization_name' => 'required|string', 'organization_vanity_name' => 'nullable|string', 'organization_logo' => 'nullable|string', ]); $pendingData = session('linkedin_page_pending'); if (! $pendingData) { return $this->popupCallback(false, __('accounts.popup_callback.session_expired'), $this->platform->value); } $workspace = Workspace::find($pendingData['workspace_id']); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { return $this->popupCallback(false, __('accounts.popup_callback.workspace_not_found'), $this->platform->value); } try { $avatarPath = uploadFromUrl($request->organization_logo); $reconnectId = $pendingData['reconnect_id'] ?? null; if ($reconnectId) { // Reconnect existing account $existingAccount = $workspace->socialAccounts()->find($reconnectId); if ($existingAccount) { $existingAccount->update([ 'platform_user_id' => $request->organization_id, 'username' => $request->organization_vanity_name, 'display_name' => $request->organization_name, 'avatar_url' => $avatarPath, 'access_token' => $pendingData['token'], 'refresh_token' => $pendingData['refresh_token'], 'token_expires_at' => $pendingData['expires_in'] ? now()->addSeconds($pendingData['expires_in']) : null, // LinkedIn returns scope CSV-joined but Socialite splits on space, so re-split here. 'scopes' => explode(',', implode(',', $pendingData['approved_scopes'] ?? [])), 'meta' => [ 'organization_id' => $request->organization_id, 'admin_user_id' => $pendingData['user_id'], 'admin_name' => $pendingData['name'], ], ]); $existingAccount->markAsConnected(); // Sync tokens to LinkedIn personal if it exists app(LinkedInTokenSynchronizer::class)->syncTokens($existingAccount); session()->forget(['linkedin_page_pending', 'linkedin_page_reconnect_id']); return $this->popupCallback(true, __('accounts.popup_callback.reconnected'), $this->platform->value); } } $account = $workspace->socialAccounts()->updateOrCreate( [ 'platform' => $this->platform->value, 'platform_user_id' => $request->organization_id, ], [ 'username' => $request->organization_vanity_name, 'display_name' => $request->organization_name, 'avatar_url' => $avatarPath, 'access_token' => $pendingData['token'], 'refresh_token' => $pendingData['refresh_token'], 'token_expires_at' => $pendingData['expires_in'] ? now()->addSeconds($pendingData['expires_in']) : null, // LinkedIn returns scope CSV-joined but Socialite splits on space, so re-split here. 'scopes' => explode(',', implode(',', $pendingData['approved_scopes'] ?? [])), 'status' => Status::Connected, 'error_message' => null, 'disconnected_at' => null, 'meta' => [ 'organization_id' => $request->organization_id, 'admin_user_id' => $pendingData['user_id'], 'admin_name' => $pendingData['name'], ], ], ); // Sync tokens to LinkedIn personal if it exists app(LinkedInTokenSynchronizer::class)->syncTokens($account); session()->forget(['linkedin_page_pending', 'linkedin_page_reconnect_id']); return $this->popupCallback(true, __('accounts.popup_callback.connected'), $this->platform->value); } catch (\Exception $e) { Log::error('LinkedIn Page selection error', [ 'error' => $e->getMessage(), ]); return $this->popupCallback(false, __('accounts.popup_callback.error_connecting_page'), $this->platform->value); } } private function fetchOrganizations(string $accessToken): array { $response = Http::withToken($accessToken) ->get(config('trypost.platforms.linkedin-page.api').'/v2/organizationAcls', [ 'q' => 'roleAssignee', 'role' => 'ADMINISTRATOR', 'projection' => '(elements*(organization~(id,localizedName,vanityName,logoV2(original~:playableStreams))))', ]); if ($response->failed()) { Log::error('LinkedIn Organizations fetch error', [ 'error' => $response->body(), ]); return []; } $data = $response->json(); $organizations = []; foreach (data_get($data, 'elements', []) as $element) { $org = data_get($element, 'organization~', null); if ($org) { $logoUrl = null; $logoUrl = data_get($org, 'logoV2.original~.elements.0.identifiers.0.identifier'); $organizations[] = [ 'id' => data_get($org, 'id'), 'name' => data_get($org, 'localizedName', 'Unknown'), 'vanity_name' => data_get($org, 'vanityName', null), 'logo' => $logoUrl, ]; } } return $organizations; } }