mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 02:27:57 +00:00
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.
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
(<code>Settings > Webhooks</code>):
|
||||
</div>
|
||||
<x-forms.input readonly label="Webhook URL"
|
||||
value="{{ $webhook_endpoint }}/webhooks/source/gitlab/events" />
|
||||
value="{{ rtrim($this->resolvePublicBaseUrl(), '/') }}/webhooks/source/gitlab/events" />
|
||||
<x-forms.input canGate="update" :canResource="$gitlab_app" id="webhookToken" label="Webhook Secret Token"
|
||||
helper="Set this same token in your GitLab webhook's 'Secret token' field." />
|
||||
</div>
|
||||
@@ -148,7 +148,51 @@
|
||||
<span>You must complete this step before you can use this source!</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-4" x-data="{
|
||||
webhookEndpoint: $wire.entangle('webhook_endpoint').live,
|
||||
useCustomWebhookEndpoint: $wire.entangle('use_custom_webhook_endpoint').live,
|
||||
customWebhookEndpoint: $wire.entangle('custom_webhook_endpoint').live,
|
||||
redirectPath: '/webhooks/source/gitlab/redirect',
|
||||
get redirectUri() {
|
||||
const base = (this.useCustomWebhookEndpoint ? this.customWebhookEndpoint : this.webhookEndpoint) || '';
|
||||
return base ? base.replace(/\/+$/, '') + this.redirectPath : '';
|
||||
}
|
||||
}">
|
||||
@if (!isCloud() || isDev())
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3>Public endpoint</h3>
|
||||
<div class="text-sm dark:text-neutral-400">
|
||||
GitLab will redirect back to this Coolify URL. It must match the Callback URL on your GitLab OAuth Application exactly.
|
||||
</div>
|
||||
<x-forms.checkbox x-model="useCustomWebhookEndpoint" id="use_custom_webhook_endpoint"
|
||||
label="Use custom webhook endpoint"
|
||||
helper="Enable this when the public URL GitLab should call differs from Coolify's configured URL, for example behind Cloudflare Tunnel or when accessing via a LAN IP." />
|
||||
<div x-show="!useCustomWebhookEndpoint">
|
||||
<x-forms.select wire:model.live='webhook_endpoint' x-model="webhookEndpoint"
|
||||
label="Selected endpoint"
|
||||
helper="GitLab will use this endpoint unless custom mode is enabled.">
|
||||
@if ($fqdn)
|
||||
<option value="{{ $fqdn }}">Use {{ $fqdn }}</option>
|
||||
@endif
|
||||
@if ($ipv4)
|
||||
<option value="{{ $ipv4 }}">Use {{ $ipv4 }}</option>
|
||||
@endif
|
||||
@if ($ipv6)
|
||||
<option value="{{ $ipv6 }}">Use {{ $ipv6 }}</option>
|
||||
@endif
|
||||
@if (config('app.url'))
|
||||
<option value="{{ config('app.url') }}">Use {{ config('app.url') }}</option>
|
||||
@endif
|
||||
</x-forms.select>
|
||||
</div>
|
||||
<div x-cloak x-show="useCustomWebhookEndpoint">
|
||||
<x-forms.input x-model="customWebhookEndpoint" id="custom_webhook_endpoint" type="url"
|
||||
label="Custom endpoint" placeholder="https://coolify.example.com"
|
||||
helper="GitLab will use this custom public URL. Do not include /webhooks." />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<h3>Step 1: Create an OAuth Application on GitLab</h3>
|
||||
<div class="text-sm flex flex-col gap-1">
|
||||
<p>Go to your GitLab instance and create a new OAuth Application:</p>
|
||||
@@ -158,7 +202,9 @@
|
||||
<x-external-link />
|
||||
</a>
|
||||
<ul class="list-disc list-inside mt-2 space-y-1">
|
||||
<li>Set <strong>Redirect URI</strong> to: <code>{{ $redirectUri }}</code></li>
|
||||
<li>Set <strong>Redirect URI</strong> to:
|
||||
<code x-text="redirectUri || @js($redirectUri)">{{ $redirectUri }}</code>
|
||||
</li>
|
||||
<li>Enable scopes: <code>api</code>, <code>read_user</code>, <code>read_repository</code></li>
|
||||
<li>Uncheck <strong>Confidential</strong> if you run into issues</li>
|
||||
</ul>
|
||||
@@ -221,8 +267,8 @@
|
||||
|
||||
@if ($clientId)
|
||||
<h3 class="pt-2">Step 3: Authorize with GitLab</h3>
|
||||
<div class="text-sm">Click the button below to authorize Coolify with your GitLab instance.</div>
|
||||
<a href="{{ $this->getOAuthUrl() }}" class="w-fit">
|
||||
<div class="text-sm">Click the button below to authorize Coolify with your GitLab instance. The redirect URI must match the Callback URL configured in GitLab.</div>
|
||||
<a href="{{ $this->getOAuthUrl() }}" class="w-fit" wire:key="oauth-url-{{ md5((string) $redirectUri) }}">
|
||||
<x-forms.button class="mt-2">
|
||||
Connect to GitLab
|
||||
</x-forms.button>
|
||||
|
||||
@@ -73,4 +73,17 @@ describe('GitLab source setup view', function () {
|
||||
->assertSet('clientId', 'gitlab-app-id')
|
||||
->assertSet('clientSecretInput', 'super-secret-value');
|
||||
});
|
||||
|
||||
test('supports github-style custom public endpoint for oauth redirect uri', function () {
|
||||
Livewire::withQueryParams(['gitlab_app_uuid' => $this->gitlabApp->uuid])
|
||||
->test(Change::class)
|
||||
->assertSee('Use custom webhook endpoint')
|
||||
->assertSee('Selected endpoint')
|
||||
->set('use_custom_webhook_endpoint', true)
|
||||
->set('custom_webhook_endpoint', 'http://100.75.155.70:8000')
|
||||
->assertSet('redirectUri', 'http://100.75.155.70:8000/webhooks/source/gitlab/redirect');
|
||||
|
||||
expect($this->gitlabApp->refresh()->redirect_uri)
|
||||
->toBe('http://100.75.155.70:8000/webhooks/source/gitlab/redirect');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user