From eaa0b0156dac5efeeefa9a9d16a794a57930d3de Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 21 Jul 2026 21:11:20 +0200
Subject: [PATCH] feat(gitlab): add custom public endpoint for OAuth redirect
Match the GitHub App endpoint picker so self-hosted / tunnel setups can
select FQDN, IP, app URL, or a custom base. Redirect URI is derived as
{base}/webhooks/source/gitlab/redirect and persisted for token exchange.
---
app/Livewire/Source/Gitlab/Change.php | 80 ++++++++++++++++++-
.../livewire/source/gitlab/change.blade.php | 56 +++++++++++--
tests/Feature/GitlabSourceChangeViewTest.php | 13 +++
3 files changed, 143 insertions(+), 6 deletions(-)
diff --git a/app/Livewire/Source/Gitlab/Change.php b/app/Livewire/Source/Gitlab/Change.php
index 5a386b429..bab6a41d1 100644
--- a/app/Livewire/Source/Gitlab/Change.php
+++ b/app/Livewire/Source/Gitlab/Change.php
@@ -17,6 +17,10 @@ class Change extends Component
public string $webhook_endpoint = '';
+ public string $custom_webhook_endpoint = '';
+
+ public bool $use_custom_webhook_endpoint = false;
+
public ?string $ipv4 = null;
public ?string $ipv6 = null;
@@ -75,6 +79,9 @@ class Change extends Component
'groupName' => 'nullable|string',
'isSystemWide' => 'required|bool',
'privateKeyId' => 'nullable|int',
+ 'webhook_endpoint' => ['required', 'string', 'url'],
+ 'custom_webhook_endpoint' => ['nullable', 'string', 'url'],
+ 'use_custom_webhook_endpoint' => ['required', 'bool'],
];
}
@@ -91,6 +98,40 @@ class Change extends Component
}
}
+ public function updatedWebhookEndpoint(): void
+ {
+ $this->persistRedirectUriFromEndpoint();
+ }
+
+ public function updatedUseCustomWebhookEndpoint(): void
+ {
+ $this->persistRedirectUriFromEndpoint();
+ }
+
+ public function updatedCustomWebhookEndpoint(): void
+ {
+ $this->persistRedirectUriFromEndpoint();
+ }
+
+ private function persistRedirectUriFromEndpoint(): void
+ {
+ $this->refreshRedirectUri();
+
+ if (! $this->gitlab_app || blank($this->redirectUri)) {
+ return;
+ }
+
+ try {
+ $this->authorize('update', $this->gitlab_app);
+ if ($this->gitlab_app->redirect_uri !== $this->redirectUri) {
+ $this->gitlab_app->redirect_uri = $this->redirectUri;
+ $this->gitlab_app->save();
+ }
+ } catch (\Throwable) {
+ // Keep the live redirect URI even if the user cannot persist yet.
+ }
+ }
+
public function mount()
{
try {
@@ -124,7 +165,25 @@ class Change extends Component
$this->webhook_endpoint = $this->fqdn ?? $this->ipv4 ?? $this->ipv6 ?? config('app.url') ?? '';
}
- $this->redirectUri = $this->webhook_endpoint.'/webhooks/source/gitlab/redirect';
+ // Prefer a previously saved redirect base when it matches one of the selectable endpoints
+ // or when it differs (restore custom mode for self-hosted / tunnel setups).
+ $savedRedirect = $this->gitlab_app->redirect_uri;
+ if (filled($savedRedirect)) {
+ $savedBase = rtrim(str($savedRedirect)->before('/webhooks/source/gitlab/redirect')->toString(), '/');
+ $known = collect([$this->fqdn, $this->ipv4, $this->ipv6, config('app.url')])
+ ->filter()
+ ->map(fn ($url) => rtrim((string) $url, '/'));
+
+ if ($known->contains($savedBase)) {
+ $this->webhook_endpoint = $savedBase;
+ $this->use_custom_webhook_endpoint = false;
+ } elseif (! (isCloud() && ! isDev()) && filled($savedBase)) {
+ $this->use_custom_webhook_endpoint = true;
+ $this->custom_webhook_endpoint = $savedBase;
+ }
+ }
+
+ $this->refreshRedirectUri();
$this->oauthState = $this->createOAuthState();
} catch (\Throwable $e) {
@@ -132,6 +191,23 @@ class Change extends Component
}
}
+ public function refreshRedirectUri(): void
+ {
+ $base = $this->resolvePublicBaseUrl();
+ $this->redirectUri = $base === ''
+ ? ''
+ : $base.'/webhooks/source/gitlab/redirect';
+ }
+
+ public function resolvePublicBaseUrl(): string
+ {
+ if ($this->use_custom_webhook_endpoint && filled($this->custom_webhook_endpoint)) {
+ return rtrim($this->custom_webhook_endpoint, '/');
+ }
+
+ return rtrim($this->webhook_endpoint ?: (config('app.url') ?? ''), '/');
+ }
+
public static function oauthStateCacheKey(string $state): string
{
return 'gitlab-app-oauth-state:'.hash('sha256', $state);
@@ -165,6 +241,7 @@ class Change extends Component
$this->gitlab_app->group_name = $this->groupName;
$this->gitlab_app->is_system_wide = $this->isSystemWide;
$this->gitlab_app->private_key_id = $this->privateKeyId;
+ $this->refreshRedirectUri();
$this->gitlab_app->redirect_uri = $this->redirectUri;
} else {
$this->name = $this->gitlab_app->name;
@@ -280,6 +357,7 @@ class Change extends Component
public function getOAuthUrl(): string
{
+ $this->refreshRedirectUri();
$baseUrl = rtrim($this->htmlUrl, '/');
$query = http_build_query([
diff --git a/resources/views/livewire/source/gitlab/change.blade.php b/resources/views/livewire/source/gitlab/change.blade.php
index 5cb859ec5..cd219f7d3 100644
--- a/resources/views/livewire/source/gitlab/change.blade.php
+++ b/resources/views/livewire/source/gitlab/change.blade.php
@@ -98,7 +98,7 @@
(Settings > Webhooks):
Go to your GitLab instance and create a new OAuth Application:
@@ -158,7 +202,9 @@{{ $redirectUri }}{{ $redirectUri }}
+ api, read_user, read_repository