ensurePlatformEnabled(); $workspace = $request->user()->currentWorkspace; if (! $workspace) { return redirect()->route('app.workspaces.create'); } $this->authorize('manageAccounts', $workspace); $this->ensureSocialAccountLimit($workspace); return Inertia::render('accounts/MastodonConnect', [ 'errors' => session('errors')?->getBag('default')?->toArray() ?? [], ]); } /** * Register app on instance and redirect to OAuth */ public function authorizeInstance(Request $request): SymfonyResponse|RedirectResponse { $this->ensurePlatformEnabled(); $request->validate([ 'instance' => 'required|url', ]); $workspace = $request->user()->currentWorkspace; if (! $workspace) { return redirect()->route('app.workspaces.create'); } $this->authorize('manageAccounts', $workspace); $instance = rtrim($request->instance, '/'); try { // Register app on the instance $appResponse = Http::post("{$instance}/api/v1/apps", [ 'client_name' => config('app.name'), 'redirect_uris' => route('app.social.mastodon.callback'), 'scopes' => self::SCOPES, 'website' => config('app.url'), ]); if ($appResponse->failed()) { Log::error('Mastodon app registration failed', [ 'instance' => $instance, 'status' => $appResponse->status(), 'body' => $appResponse->body(), ]); return back()->withErrors(['instance' => 'Could not connect to this Mastodon instance.']); } $app = $appResponse->json(); // Store in session for callback $state = bin2hex(random_bytes(16)); session([ 'mastodon_instance' => $instance, 'mastodon_client_id' => $app['client_id'], 'mastodon_client_secret' => $app['client_secret'], 'mastodon_oauth_state' => $state, 'social_connect_workspace' => $workspace->id, ]); // Redirect to OAuth $params = http_build_query([ 'client_id' => $app['client_id'], 'response_type' => 'code', 'redirect_uri' => route('app.social.mastodon.callback'), 'scope' => self::SCOPES, 'state' => $state, ]); return Inertia::location("{$instance}/oauth/authorize?{$params}"); } catch (\Exception $e) { Log::error('Mastodon connection error', [ 'instance' => $instance, 'error' => $e->getMessage(), ]); return back()->withErrors(['instance' => 'Error connecting to Mastodon instance.']); } } /** * Handle OAuth callback */ public function callback(Request $request): View { $workspaceId = session('social_connect_workspace'); $savedState = session('mastodon_oauth_state'); $instance = session('mastodon_instance'); $clientId = session('mastodon_client_id'); $clientSecret = session('mastodon_client_secret'); if (! $workspaceId || ! $instance) { $this->clearMastodonSession(); return $this->popupCallback(false, 'Session expired. Please try again.', $this->platform->value); } if ($request->state !== $savedState) { $this->clearMastodonSession(); return $this->popupCallback(false, 'Invalid state. Please try again.', $this->platform->value); } $workspace = Workspace::find($workspaceId); if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) { $this->clearMastodonSession(); return $this->popupCallback(false, 'Workspace not found.', $this->platform->value); } try { // Exchange code for token $tokenResponse = Http::asForm()->post("{$instance}/oauth/token", [ 'grant_type' => 'authorization_code', 'client_id' => $clientId, 'client_secret' => $clientSecret, 'redirect_uri' => route('app.social.mastodon.callback'), 'code' => $request->code, ]); if ($tokenResponse->failed()) { Log::error('Mastodon token exchange failed', [ 'status' => $tokenResponse->status(), 'body' => $tokenResponse->body(), ]); $this->clearMastodonSession(); return $this->popupCallback(false, 'Failed to authenticate.', $this->platform->value); } $tokenData = $tokenResponse->json(); $accessToken = $tokenData['access_token']; // Get user profile $profileResponse = Http::withToken($accessToken) ->get("{$instance}/api/v1/accounts/verify_credentials"); if ($profileResponse->failed()) { $this->clearMastodonSession(); return $this->popupCallback(false, 'Failed to get profile.', $this->platform->value); } $profile = $profileResponse->json(); // Check existing $existingAccount = $workspace->socialAccounts() ->where('platform', $this->platform->value) ->first(); if ($existingAccount && ! $existingAccount->isDisconnected()) { $this->clearMastodonSession(); return $this->popupCallback(false, 'Mastodon is already connected.', $this->platform->value); } $avatarPath = data_get($profile, 'avatar') ? uploadFromUrl(data_get($profile, 'avatar')) : null; $accountData = [ 'platform' => $this->platform->value, 'platform_user_id' => data_get($profile, 'id'), 'username' => data_get($profile, 'acct'), 'display_name' => data_get($profile, 'display_name') ?: data_get($profile, 'username'), 'avatar_url' => $avatarPath, 'access_token' => $accessToken, 'refresh_token' => null, // Mastodon tokens don't expire 'token_expires_at' => null, 'meta' => [ 'instance' => $instance, 'client_id' => $clientId, 'client_secret' => $clientSecret, ], ]; if ($existingAccount) { $existingAccount->update($accountData); $existingAccount->markAsConnected(); $this->clearMastodonSession(); return $this->popupCallback(true, 'Mastodon account reconnected!', $this->platform->value); } $accountData['status'] = Status::Connected; $workspace->socialAccounts()->create($accountData); $this->clearMastodonSession(); return $this->popupCallback(true, 'Mastodon account connected!', $this->platform->value); } catch (\Exception $e) { Log::error('Mastodon callback error', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); $this->clearMastodonSession(); return $this->popupCallback(false, 'Error connecting account.', $this->platform->value); } } private function clearMastodonSession(): void { session()->forget([ 'mastodon_instance', 'mastodon_client_id', 'mastodon_client_secret', 'mastodon_oauth_state', 'social_connect_workspace', ]); } }