mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05:00
Reapply "Merge branch 'next' into main"
This reverts commit 7bbd91175f.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { initializeCopyButtonComponent } from './copy-button.js';
|
||||
import { initializeTerminalComponent } from './terminal.js';
|
||||
|
||||
// Livewire 3.5.19+ re-applies `x-cloak` to morphed elements during wire:navigate
|
||||
@@ -12,6 +13,7 @@ document.addEventListener('livewire:navigated', () => {
|
||||
// Keeping this registration independent from the current route also makes it
|
||||
// available before Alpine processes terminal markup after wire:navigate.
|
||||
document.addEventListener('alpine:init', initializeTerminalComponent);
|
||||
document.addEventListener('alpine:init', initializeCopyButtonComponent);
|
||||
|
||||
/**
|
||||
* Smooth-scroll a settings section into view, then flash its border for 500ms
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Alpine data provider for the <x-copy-button> component (x-data="copyButton").
|
||||
export function initializeCopyButtonComponent() {
|
||||
window.Alpine.data('copyButton', () => ({
|
||||
copied: false,
|
||||
async copy(value) {
|
||||
if (value === null || value === undefined) {
|
||||
window.toast('Value is not available.', { type: 'warning' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (navigator.clipboard?.writeText && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
} else {
|
||||
// Deprecated, but the only copy path on plain http (non-secure contexts).
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = value;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
if (!ok) {
|
||||
throw new Error('Copy command was rejected.');
|
||||
}
|
||||
}
|
||||
this.copied = true;
|
||||
setTimeout(() => (this.copied = false), 1200);
|
||||
} catch (e) {
|
||||
window.toast('Could not copy to clipboard.', { type: 'warning' });
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -80,11 +80,15 @@
|
||||
|
||||
@if ($enabled_oauth_providers->isNotEmpty())
|
||||
<div class="auth-divider"><span>Or continue with</span></div>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
@foreach ($enabled_oauth_providers as $provider_setting)
|
||||
<x-forms.button class="w-full justify-center" type="button"
|
||||
onclick="document.location.href='/auth/{{ $provider_setting->provider }}/redirect'">
|
||||
{{ __("auth.login.$provider_setting->provider") }}
|
||||
@if ($provider_setting->provider !== 'oidc')
|
||||
<img class="size-5 shrink-0 dark:invert"
|
||||
src="{{ asset('svgs/'.$provider_setting->provider.'.svg') }}" alt="" aria-hidden="true">
|
||||
@endif
|
||||
{{ $provider_setting->loginLabel() }}
|
||||
</x-forms.button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
@props([
|
||||
'value',
|
||||
'value' => null,
|
||||
'resolve' => null,
|
||||
'label' => 'Copy to clipboard',
|
||||
])
|
||||
|
||||
<button type="button"
|
||||
x-data="{ copied: false }"
|
||||
x-on:click.prevent.stop="await window.copyToClipboard({{ Js::from($value) }}); copied = true; setTimeout(() => copied = false, 1000)"
|
||||
{{ $attributes->class('inline-flex size-6 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-40 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-white') }}
|
||||
title="{{ $label }}" aria-label="{{ $label }}" @disabled(blank($value))>
|
||||
<svg x-show="!copied" class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
aria-hidden="true">
|
||||
<path d="M8 8.75H6.5A2.25 2.25 0 0 0 4.25 11v6.5a2.25 2.25 0 0 0 2.25 2.25H13a2.25 2.25 0 0 0 2.25-2.25V16"
|
||||
stroke-width="1.5" stroke-linecap="round" />
|
||||
<rect x="8.75" y="4.25" width="11" height="11" rx="2.25" stroke-width="1.5" />
|
||||
</svg>
|
||||
<svg x-show="copied" x-cloak class="size-3.5 text-green-500" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" aria-hidden="true">
|
||||
<path d="m6.75 12.25 3.5 3.5 7-7" stroke-width="1.5" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
@php
|
||||
$valueExpression = $resolve ?? \Illuminate\Support\Js::from($value);
|
||||
@endphp
|
||||
|
||||
<button type="button" title="{{ $label }}" aria-label="{{ $label }}"
|
||||
{{ $attributes->class(['icon-button group shrink-0']) }} @disabled($resolve === null && blank($value))
|
||||
x-data="copyButton" @click="copy(await ({{ $valueExpression }}))">
|
||||
<span class="inline-flex transition-transform duration-150 ease-out group-active:scale-75">
|
||||
<x-reicon name="copy" x-show="!copied" class="size-3.5" />
|
||||
<x-reicon name="check" x-cloak x-show="copied" class="size-3.5 text-success"
|
||||
x-transition:enter="transition-transform duration-200 ease-out"
|
||||
x-transition:enter-start="scale-50" x-transition:enter-end="scale-100" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
@props(['text', 'label' => null])
|
||||
|
||||
<div class="w-full" x-data="{ copied: false }">
|
||||
@if ($label)
|
||||
<label class="flex gap-1 items-center mb-1 text-sm font-medium text-black dark:text-white">{{ $label }}</label>
|
||||
@endif
|
||||
<div class="relative">
|
||||
<input type="text" value="{{ $text }}"
|
||||
class="input input-with-copy-button bg-white dark:bg-coolgray-100 dark:read-only:bg-coolgray-100 dark:read-only:text-white"
|
||||
readonly
|
||||
@keydown.prevent @paste.prevent @cut.prevent @drop.prevent
|
||||
@focus="$event.target.select()">
|
||||
<button
|
||||
type="button"
|
||||
@click.prevent="await window.copyToClipboard({{ Js::from($text) }}); copied = true; setTimeout(() => copied = false, 1000)"
|
||||
class="copy-button flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer text-neutral-500 transition-colors hover:text-black focus-visible:ring-2 focus-visible:ring-coollabs focus-visible:ring-offset-2 dark:text-neutral-400 dark:hover:text-white dark:focus-visible:ring-warning dark:focus-visible:ring-offset-base"
|
||||
title="Copy to clipboard"
|
||||
aria-label="Copy to clipboard">
|
||||
<svg x-show="!copied" class="size-[18px]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<svg x-show="copied" class="size-[18px] text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
@props(['text', 'label' => null])
|
||||
|
||||
<div class="w-full">
|
||||
@if ($label)
|
||||
<label class="flex gap-1 items-center mb-1 text-sm font-medium text-black dark:text-white">{{ $label }}</label>
|
||||
@endif
|
||||
<div class="relative">
|
||||
<input type="text" value="{{ $text }}"
|
||||
class="input input-with-copy-button bg-white dark:bg-coolgray-100 dark:read-only:bg-coolgray-100 dark:read-only:text-white"
|
||||
readonly
|
||||
@keydown.prevent @paste.prevent @cut.prevent @drop.prevent
|
||||
@focus="$event.target.select()">
|
||||
<x-copy-button :value="$text" class="absolute top-1/2 right-2 -translate-y-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,17 +287,8 @@
|
||||
<div class="relative mb-2" x-data="{ decodedText: confirmationText }">
|
||||
<div class="relative">
|
||||
<input type="text" x-model="decodedText" readonly class="input">
|
||||
<button x-show="window.isSecureContext"
|
||||
@click.prevent="navigator.clipboard.writeText(decodedText); $el.innerHTML = '<svg class=\'w-5 h-5 text-green-500\' fill=\'none\' stroke=\'currentColor\' viewBox=\'0 0 24 24\'><path stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M5 13l4 4L19 7\' /></svg>'; setTimeout(() => $el.innerHTML = '<svg class=\'w-5 h-5\' fill=\'none\' stroke=\'currentColor\' viewBox=\'0 0 24 24\'><path stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\' /></svg>', 1000)"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-300 transition-colors"
|
||||
title="Copy to clipboard">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<x-copy-button resolve="decodedText"
|
||||
class="absolute top-1/2 right-2 -translate-y-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
'upload' => '<path d="M11.4697 3.46967C11.7626 3.17678 12.2374 3.17678 12.5303 3.46967L16.5303 7.46967C16.8232 7.76256 16.8232 8.23744 16.5303 8.53033C16.2374 8.82322 15.7626 8.82322 15.4697 8.53033L12.75 5.81066V14C12.75 14.4142 12.4142 14.75 12 14.75C11.5858 14.75 11.25 14.4142 11.25 14V5.81066L8.53033 8.53033C8.23744 8.82322 7.76256 8.82322 7.46967 8.53033C7.17678 8.23744 7.17678 7.76256 7.46967 7.46967L11.4697 3.46967Z" fill="currentColor"/><path d="M4 14.25C4.41421 14.25 4.75 14.5858 4.75 15V17C4.75 18.5188 5.98122 19.75 7.5 19.75H16.5C18.0188 19.75 19.25 18.5188 19.25 17V15C19.25 14.5858 19.5858 14.25 20 14.25C20.4142 14.25 20.75 14.5858 20.75 15V17C20.75 19.3472 18.8472 21.25 16.5 21.25H7.5C5.15279 21.25 3.25 19.3472 3.25 17V15C3.25 14.5858 3.58579 14.25 4 14.25Z" fill="currentColor"/>',
|
||||
'x' => '<path d="M18.4697 19.5303C18.7626 19.8232 19.2374 19.8232 19.5303 19.5303C19.8232 19.2374 19.8232 18.7626 19.5303 18.4697L13.0607 12L19.5303 5.53033C19.8232 5.23744 19.8232 4.76256 19.5303 4.46967C19.2374 4.17678 18.7626 4.17678 18.4697 4.46967L12 10.9393L5.53033 4.46967C5.23744 4.17678 4.76256 4.17678 4.46967 4.46967C4.17678 4.76256 4.17678 5.23744 4.46967 5.53033L10.9393 12L4.46967 18.4697C4.17678 18.7626 4.17678 19.2374 4.46967 19.5303C4.76256 19.8232 5.23744 19.8232 5.53033 19.5303L12 13.0607L18.4697 19.5303Z" fill="currentColor"/>',
|
||||
'check' => '<path d="M21.5303 5.46967C21.8232 5.76256 21.8232 6.23744 21.5303 6.53033L9.53033 18.5303C9.23744 18.8232 8.76256 18.8232 8.46967 18.5303L2.46967 12.5303C2.17678 12.2374 2.17678 11.7626 2.46967 11.4697C2.76256 11.1768 3.23744 11.1768 3.53033 11.4697L9 16.9393L20.4697 5.46967C20.7626 5.17678 21.2374 5.17678 21.5303 5.46967Z" fill="currentColor"/>',
|
||||
'copy' => '<path d="M8 8.75H6.5A2.25 2.25 0 0 0 4.25 11v6.5a2.25 2.25 0 0 0 2.25 2.25H13a2.25 2.25 0 0 0 2.25-2.25V16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="8.75" y="4.25" width="11" height="11" rx="2.25" stroke="currentColor" stroke-width="1.5"/>',
|
||||
'chevron-down' => '<g transform="scale(1.33333)"><polyline points="15.25 6.5 9 12.75 2.75 6.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></polyline></g>',
|
||||
'trash' => '<path fill-rule="evenodd" clip-rule="evenodd" d="M15.0924 1.25H8.90788C7.33861 1.24998 6.08032 1.24996 5.10577 1.38767C4.09802 1.53007 3.25979 1.83575 2.64218 2.55292C2.02457 3.27008 1.84661 4.14438 1.85528 5.1621C1.86366 6.1463 2.05033 7.39066 2.28314 8.94256L3.49937 17.0508C3.67587 18.2275 3.81878 19.1804 4.02849 19.9262C4.24683 20.7027 4.56045 21.3453 5.13662 21.8415C5.71279 22.3377 6.39485 22.5525 7.19513 22.6533C7.96377 22.75 8.92732 22.75 10.1173 22.75H13.883C15.073 22.75 16.0365 22.75 16.8052 22.6533C17.6054 22.5525 18.2875 22.3377 18.8637 21.8415C19.4398 21.3453 19.7535 20.7027 19.9718 19.9262C20.1815 19.1805 20.3244 18.2276 20.5009 17.0509L21.7172 8.94253C21.95 7.39065 22.1366 6.14629 22.145 5.1621C22.1537 4.14438 21.9757 3.27008 21.3581 2.55292C20.7405 1.83575 19.9023 1.53007 18.8945 1.38767C17.92 1.24996 16.6617 1.24998 15.0924 1.25ZM3.77879 3.53175C4.05882 3.20658 4.47927 2.9911 5.31565 2.87292C6.17295 2.75177 7.32479 2.75 8.96727 2.75H15.033C16.6755 2.75 17.8273 2.75177 18.6846 2.87292C19.521 2.9911 19.9415 3.20658 20.2215 3.53175C20.5015 3.85692 20.6523 4.30468 20.6451 5.14933C20.6448 5.18248 20.6443 5.21604 20.6435 5.25H20.5005C20.5003 5.25 20.5007 5.25 20.5005 5.25H7.00045C7.00025 5.25 7.00065 5.25 7.00045 5.25H3.35678C3.35603 5.21603 3.35551 5.18248 3.35522 5.14933C3.34803 4.30468 3.49876 3.85692 3.77879 3.53175ZM5.18949 6.75H3.48546C3.53687 7.15852 3.60161 7.61096 3.67631 8.1155L3.75015 8.18934L5.18949 6.75ZM4.05013 10.6106L4.6686 14.7338L6.37599 12.9365L4.05013 10.6106ZM5.15659 17.9593C5.17275 18.0594 5.18872 18.1563 5.20463 18.25H5.39887L5.15659 17.9593ZM6.99527 19.75C6.99879 19.75 7.00232 19.75 7.00584 19.75H13.9972C13.9991 19.75 14.0009 19.75 14.0027 19.75H18.4577C18.299 20.2287 18.1176 20.5044 17.8848 20.7049C17.6171 20.9355 17.261 21.0841 16.6178 21.165C15.9538 21.2486 15.0849 21.25 13.833 21.25H10.1673C8.91538 21.25 8.04651 21.2486 7.38247 21.165C6.73934 21.0841 6.38321 20.9355 6.11546 20.7049C5.88266 20.5044 5.70127 20.2287 5.54256 19.75H6.99527ZM15.7131 18.25H18.1895L16.9018 16.9623L15.7131 18.25ZM19.0007 16.9399C19.0087 16.8869 19.0168 16.8332 19.0249 16.7788L19.404 14.2515L17.92 15.8592L19.0007 16.9399ZM19.856 11.2381L20.2249 8.77879C20.3197 8.14673 20.4033 7.5881 20.4704 7.09045L18.16 9.40079L19.856 11.2381ZM18.6895 6.75H15.7131L17.1418 8.2977L18.6895 6.75ZM12.2532 6.75H8.81081L10.5761 8.51531L12.2532 6.75ZM11.6895 18.25H9.31081L10.5002 17.0607L11.6895 18.25ZM7.40946 11.8486L4.81081 9.25L7.00015 7.06066L9.54266 9.60317L7.40946 11.8486ZM8.47047 12.9097L10.6037 10.6642L12.6895 12.75L10.5002 14.9393L8.47047 12.9097ZM11.5608 16L13.7502 13.8107L15.8403 15.9008L13.7385 18.1777L11.5608 16ZM14.8108 12.75L16.8585 14.7977L18.9795 12.5L17.0985 10.4623L14.8108 12.75ZM13.7502 11.6893L16.0803 9.35921L13.9923 7.09721L11.6371 9.57632L13.7502 11.6893ZM7.437 13.9975L9.43949 16L7.27782 18.1617L5.50363 16.0326L7.437 13.9975Z" fill="currentColor"/>',
|
||||
'external-link' => '<path d="M13 11L21.2 2.80005" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 6.8V2H17.2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22H15C20 22 22 20 22 15V13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
'active' => request()->routeIs('security.cloud-tokens*'),
|
||||
'icon' => 'cloud',
|
||||
] : null,
|
||||
auth()->user()?->can('viewAny', App\Models\IntegrationToken::class) ? [
|
||||
'label' => 'Integration Tokens',
|
||||
'route' => 'security.integration-tokens',
|
||||
'active' => request()->routeIs('security.integration-tokens'),
|
||||
'icon' => 'network',
|
||||
] : null,
|
||||
auth()->user()?->can('viewAny', App\Models\CloudInitScript::class) ? [
|
||||
'label' => 'Cloud-Init Scripts',
|
||||
'route' => 'security.cloud-init-scripts',
|
||||
|
||||
@@ -12,6 +12,24 @@
|
||||
'active' => $activeMenu === 'advanced',
|
||||
'icon' => 'grid',
|
||||
],
|
||||
[
|
||||
'label' => 'Authentication',
|
||||
'route' => 'settings.oauth',
|
||||
'active' => $activeMenu === 'oauth',
|
||||
'icon' => 'keys',
|
||||
],
|
||||
[
|
||||
'label' => 'Transactional Email',
|
||||
'route' => 'settings.email',
|
||||
'active' => $activeMenu === 'email',
|
||||
'icon' => 'notifications',
|
||||
],
|
||||
[
|
||||
'label' => 'Instance Backup',
|
||||
'route' => 'settings.backup',
|
||||
'active' => $activeMenu === 'backup',
|
||||
'icon' => 'database',
|
||||
],
|
||||
[
|
||||
'label' => 'Updates',
|
||||
'route' => 'settings.updates',
|
||||
|
||||
@@ -225,30 +225,6 @@
|
||||
let checkHealthInterval = null;
|
||||
let checkIfIamDeadInterval = null;
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const copied = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
if (!copied) {
|
||||
throw new Error('Copy command was rejected.');
|
||||
}
|
||||
}
|
||||
window.Livewire.dispatch('success', 'Copied to clipboard.');
|
||||
} catch (error) {
|
||||
window.Livewire.dispatch('error', 'Failed to copy to clipboard.');
|
||||
}
|
||||
}
|
||||
window.copyToClipboard = copyToClipboard;
|
||||
document.addEventListener('livewire:init', () => {
|
||||
window.Livewire.on('reloadWindow', (timeout) => {
|
||||
if (timeout) {
|
||||
|
||||
@@ -134,15 +134,22 @@
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.input id="email" label="Email" readonly />
|
||||
<x-forms.button @click="openEmailModal()" type="button"
|
||||
x-bind:disabled="emailModalOpen">
|
||||
:disabled="$uses_sso" x-bind:disabled="emailModalOpen || @js($uses_sso)">
|
||||
Change
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<template x-teleport="body">
|
||||
@if ($uses_sso)
|
||||
<x-callout type="info" title="Email managed by SSO">
|
||||
Signed in with SSO @if ($sso_provider_label) ({{ $sso_provider_label }}) @endif. Email is managed by your SSO provider.
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
@if (! $uses_sso)
|
||||
<template x-teleport="body">
|
||||
<div x-show="emailModalOpen" x-cloak
|
||||
class="fixed inset-0 z-99 flex h-screen w-screen items-center justify-center p-4">
|
||||
<div class="absolute inset-0 h-full w-full bg-black/55 backdrop-blur-[3px]"></div>
|
||||
@@ -191,7 +198,8 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@endif
|
||||
|
||||
<form wire:submit="resetPassword">
|
||||
<section class="application-settings-section">
|
||||
@@ -249,9 +257,9 @@
|
||||
</form>
|
||||
<div x-data="{ showCode: false }">
|
||||
<div x-cloak x-show="showCode" class="space-y-2 pb-3">
|
||||
<x-forms.copy-button
|
||||
<x-forms.copy-input
|
||||
text="{{ decrypt(request()->user()->two_factor_secret) }}" />
|
||||
<x-forms.copy-button text="{{ request()->user()->twoFactorQrCodeUrl() }}" />
|
||||
<x-forms.copy-input text="{{ request()->user()->twoFactorQrCodeUrl() }}" />
|
||||
</div>
|
||||
<x-forms.button type="button" x-on:click="showCode = !showCode">
|
||||
<span x-text="showCode ? 'Hide manual setup' : 'Show manual setup'"></span>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h3 class="mb-4 text-sm font-semibold text-black dark:text-fg">Internal access</h3>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@if ($currentInternalHostname)
|
||||
<x-forms.copy-button label="Internal hostname" :text="$currentInternalHostname" />
|
||||
<x-forms.copy-input label="Internal hostname" :text="$currentInternalHostname" />
|
||||
@else
|
||||
<div class="w-full">
|
||||
<label class="mb-1 flex items-center gap-1 text-sm font-medium text-black dark:text-white">Internal hostname</label>
|
||||
@@ -25,9 +25,9 @@
|
||||
readonly aria-live="polite">
|
||||
</div>
|
||||
@endif
|
||||
<x-forms.copy-button label="Docker network" :text="$application->destination->network" />
|
||||
<x-forms.copy-button label="Exposed ports" :text="$exposedPorts ?: 'None'" />
|
||||
<x-forms.copy-button label="Network aliases" :text="$networkAliases->implode(', ') ?: 'None'" />
|
||||
<x-forms.copy-input label="Docker network" :text="$application->destination->network" />
|
||||
<x-forms.copy-input label="Exposed ports" :text="$exposedPorts ?: 'None'" />
|
||||
<x-forms.copy-input label="Network aliases" :text="$networkAliases->implode(', ') ?: 'None'" />
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col gap-3 border-t border-neutral-200 pt-4 sm:flex-row sm:items-center sm:justify-between dark:border-white/[0.07]">
|
||||
<p class="text-sm text-neutral-500 dark:text-fg-dim">
|
||||
|
||||
@@ -116,25 +116,9 @@
|
||||
<p class="text-[13px] leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
Mount a Docker volume inside the container.
|
||||
</p>
|
||||
@if ($isSwarm)
|
||||
<div class="text-warning">Swarm Mode detected: You need to set a shared
|
||||
volume
|
||||
(EFS/NFS/etc) on all the worker nodes if you would like to use a
|
||||
persistent
|
||||
volumes.</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-4">
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="pv-name"
|
||||
id="name" label="Name" required helper="Volume name." />
|
||||
@if ($isSwarm)
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/root" id="host_path" label="Source Path" required
|
||||
helper="Directory on the host system." />
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/root" id="host_path" label="Source Path"
|
||||
helper="Directory on the host system." />
|
||||
@endif
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/tmp/root" id="mount_path" label="Destination Path"
|
||||
required helper="Directory inside the container." />
|
||||
|
||||
@@ -219,7 +219,8 @@
|
||||
@else
|
||||
<livewire:project.shared.environment-variable.show-hardcoded
|
||||
wire:key="{{ $row['id'] }}" :env="$row['environmentVariable']"
|
||||
:isPreview="$row['scope'] === 'preview'" :showEnvironmentType="$showEnvironmentType" />
|
||||
:isPreview="$row['scope'] === 'preview'" :showEnvironmentType="$showEnvironmentType"
|
||||
:resourceableType="get_class($resource)" :resourceableId="$resource->id" />
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
+4
-1
@@ -28,7 +28,10 @@
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
<div class="justify-self-end">
|
||||
<div class="flex items-center gap-0.5 justify-self-end">
|
||||
@unless (auth()->user()?->isMember() ?? true)
|
||||
<x-copy-button resolve="$wire.copyValue()" label="Copy value" />
|
||||
@endunless
|
||||
<x-modal-input title="Environment variable details" :closeOutside="false">
|
||||
<x-slot:content>
|
||||
<button type="button" data-env-settings-trigger class="icon-button shrink-0"
|
||||
|
||||
@@ -83,7 +83,10 @@
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
<div class="justify-self-end">
|
||||
<div class="flex items-center gap-0.5 justify-self-end">
|
||||
@if (! $isLocked && ! $isValueHidden)
|
||||
<x-copy-button resolve="$wire.copyValue()" label="Copy value" />
|
||||
@endif
|
||||
{{-- Open modal immediately (Alpine); decrypt value in a follow-up Livewire request. --}}
|
||||
<x-modal-input title="Edit environment variable" :closeOutside="false" :wireIgnore="false"
|
||||
wireOpen="editorOpen">
|
||||
|
||||
@@ -2,44 +2,7 @@
|
||||
$break = $break ?? false;
|
||||
$label = $label ?? 'Copy';
|
||||
@endphp
|
||||
<div class="flex min-w-0 items-center gap-1.5"
|
||||
x-data="{
|
||||
copied: false,
|
||||
async copy(text) {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = text;
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'fixed';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 1000);
|
||||
} catch (e) {
|
||||
console.error('Copy failed', e);
|
||||
}
|
||||
}
|
||||
}">
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<span @class(['min-w-0', 'break-all' => $break])>{{ $text }}</span>
|
||||
<button type="button"
|
||||
@click.prevent.stop="copy(@js($text))"
|
||||
class="inline-flex size-7 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-coolgray-200 dark:hover:text-white"
|
||||
title="{{ $label }}"
|
||||
aria-label="{{ $label }}">
|
||||
<svg x-show="!copied" class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<svg x-show="copied" x-cloak class="size-3.5 text-green-500" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<x-copy-button :value="$text" :label="$label" />
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div>
|
||||
<h3>Resource</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Name" :text="$resource->name ?? ''" />
|
||||
<x-forms.copy-button label="UUID" :text="$resource->uuid ?? ''" />
|
||||
<x-forms.copy-input label="Name" :text="$resource->name ?? ''" />
|
||||
<x-forms.copy-input label="UUID" :text="$resource->uuid ?? ''" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<div>
|
||||
<h3>Environment</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Name" :text="$environment_name ?? ''" />
|
||||
<x-forms.copy-button label="UUID" :text="$environment_uuid" />
|
||||
<x-forms.copy-input label="Name" :text="$environment_name ?? ''" />
|
||||
<x-forms.copy-input label="UUID" :text="$environment_uuid" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -22,8 +22,8 @@
|
||||
<div>
|
||||
<h3>Project</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Name" :text="$project_name ?? ''" />
|
||||
<x-forms.copy-button label="UUID" :text="$project_uuid" />
|
||||
<x-forms.copy-input label="Name" :text="$project_name ?? ''" />
|
||||
<x-forms.copy-input label="UUID" :text="$project_uuid" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -32,8 +32,8 @@
|
||||
<div>
|
||||
<h3>Server</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Name" :text="$server_name ?? ''" />
|
||||
<x-forms.copy-button label="UUID" :text="$server_uuid" />
|
||||
<x-forms.copy-input label="Name" :text="$server_name ?? ''" />
|
||||
<x-forms.copy-input label="UUID" :text="$server_uuid" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -43,10 +43,10 @@
|
||||
<h3>Stack Sub-Resources</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
@foreach ($stack_applications as $item)
|
||||
<x-forms.copy-button :label="'Application: ' . $item['name']" :text="$item['uuid']" />
|
||||
<x-forms.copy-input :label="'Application: ' . $item['name']" :text="$item['uuid']" />
|
||||
@endforeach
|
||||
@foreach ($stack_databases as $item)
|
||||
<x-forms.copy-button :label="'Database: ' . $item['name']" :text="$item['uuid']" />
|
||||
<x-forms.copy-input :label="'Database: ' . $item['name']" :text="$item['uuid']" />
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -154,7 +154,24 @@
|
||||
|
||||
<div class="volumes-col-source min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Source Path</span>
|
||||
<x-forms.input id="forms.{{ $id }}.hostPath" placeholder="Host path (optional)" />
|
||||
@if (filled($form['hostPath']))
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<x-forms.input id="forms.{{ $id }}.hostPath" />
|
||||
</div>
|
||||
<x-modal-confirmation title="Remove Source Path?" isErrorButton
|
||||
canGate="update" :canResource="$resource"
|
||||
buttonTitle="Remove" submitAction="clearHostPath({{ $id }})"
|
||||
:actions="[
|
||||
'Are you sure you want to remove the source path?',
|
||||
'The next deployment will use a named Docker volume instead.',
|
||||
'Data from the existing host directory will not be copied to the named volume.',
|
||||
'Use a Directory Mount when you need to mount a host directory.',
|
||||
]" />
|
||||
</div>
|
||||
@else
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="volumes-cell-dest min-w-0">
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@
|
||||
</span>
|
||||
|
||||
<span class="min-w-0">
|
||||
<x-forms.copy-button :text="$execution->filename ?? 'No archive name'" />
|
||||
<x-forms.copy-input :text="$execution->filename ?? 'No archive name'" />
|
||||
</span>
|
||||
|
||||
<span class="text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<x-external-link />
|
||||
</a>
|
||||
</x-slot:actions>
|
||||
<x-forms.copy-button label="Deploy webhook URL" :text="$deploywebhook ?? ''" />
|
||||
<x-forms.copy-input label="Deploy webhook URL" :text="$deploywebhook ?? ''" />
|
||||
</x-application.settings-section>
|
||||
|
||||
@if ($githubManualWebhook && $gitlabManualWebhook)
|
||||
@@ -70,7 +70,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Webhook URL" :text="$provider['url'] ?? ''" />
|
||||
<x-forms.copy-input label="Webhook URL" :text="$provider['url'] ?? ''" />
|
||||
@can('update', $resource)
|
||||
<x-forms.input type="password" :id="$provider['secret']"
|
||||
label="Webhook secret"
|
||||
@@ -106,7 +106,7 @@
|
||||
<x-external-link />
|
||||
</a>
|
||||
</x-slot:actions>
|
||||
<x-forms.copy-button label="Deploy webhook URL" :text="$deploywebhook ?? ''" />
|
||||
<x-forms.copy-input label="Deploy webhook URL" :text="$deploywebhook ?? ''" />
|
||||
</x-application.settings-section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -109,7 +109,12 @@
|
||||
@if (session()->has('token'))
|
||||
<x-application.settings-section title="Copy your token"
|
||||
description="This value will not be shown again after you leave this page.">
|
||||
<x-forms.copy-button :text="session('token')" />
|
||||
<div class="relative">
|
||||
<input type="text" value="{{ session('token') }}" readonly
|
||||
class="input w-full pr-12! font-mono text-[12px] text-black dark:text-fg">
|
||||
<x-copy-button :value="session('token')" label="Copy token"
|
||||
class="absolute top-1/2 right-2 -translate-y-1/2" />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<div class="w-full">
|
||||
<form class="application-settings-form flex w-full flex-col gap-4" wire:submit="save">
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input required id="name" label="Token name" />
|
||||
<x-forms.input readonly label="Provider" value="Cloudflare" />
|
||||
<div class="lg:col-span-2">
|
||||
<x-forms.input type="password" id="newToken" label="New API token"
|
||||
placeholder="Leave blank to keep the current token"
|
||||
helper="Paste a replacement token to rotate this credential." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-sm font-medium text-black dark:text-fg">Capabilities</legend>
|
||||
<div class="mt-3 rounded-lg border border-neutral-200 p-1 dark:border-white/[0.08]">
|
||||
<x-forms.checkbox id="edit-dns-capability" label="DNS" domValue="dns" fullWidth
|
||||
wire:model.live="capabilities" />
|
||||
<p class="px-2.5 pb-2 text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
Manage Cloudflare DNS records.
|
||||
</p>
|
||||
</div>
|
||||
@error('capabilities')
|
||||
<span class="text-xs text-red-500">{{ $message }}</span>
|
||||
@enderror
|
||||
</fieldset>
|
||||
|
||||
@if (in_array('dns', $capabilities, true))
|
||||
<div class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[11px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
|
||||
<div class="font-medium text-black dark:text-fg">Required Cloudflare permissions</div>
|
||||
<ul class="list-inside list-disc">
|
||||
<li>Zone - DNS - Edit</li>
|
||||
<li>Zone - Zone - Read</li>
|
||||
</ul>
|
||||
<a href="https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22edit%22%7D%5D&accountId=%2A&zoneId=all&name=Coolify%20DNS%20Management"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="font-medium text-coollabs hover:underline dark:text-warning">
|
||||
Create a replacement token in Cloudflare
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center justify-between gap-2 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
|
||||
<x-modal-confirmation title="Delete integration token?" isErrorButton buttonTitle="Delete"
|
||||
submitAction="delete" :actions="['This integration token will be permanently deleted.']"
|
||||
confirmationText="{{ $integrationToken->name }}" :confirmWithPassword="false"
|
||||
step2ButtonText="Delete token" />
|
||||
<x-forms.button type="submit" wire:target="save" isHighlighted>
|
||||
Validate and save
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="w-full">
|
||||
<form class="application-settings-form flex w-full flex-col gap-4" wire:submit="addToken">
|
||||
<x-forms.listbox required id="provider" label="Provider" :options="[
|
||||
['value' => 'cloudflare', 'label' => 'Cloudflare'],
|
||||
]" />
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input required id="name" label="Token name" placeholder="Production DNS" />
|
||||
<x-forms.input required type="password" id="token" label="API token"
|
||||
placeholder="Paste the provider token" />
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-sm font-medium text-black dark:text-fg">Capabilities</legend>
|
||||
<div class="mt-3 rounded-lg border border-neutral-200 p-1 dark:border-white/[0.08]">
|
||||
<x-forms.checkbox id="dns-capability" label="DNS" domValue="dns" fullWidth
|
||||
wire:model.live="capabilities" />
|
||||
<p class="px-2.5 pb-2 text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
Manage Cloudflare DNS records.
|
||||
</p>
|
||||
</div>
|
||||
@error('capabilities')
|
||||
<span class="text-xs text-red-500">{{ $message }}</span>
|
||||
@enderror
|
||||
</fieldset>
|
||||
|
||||
@if (in_array('dns', $capabilities, true))
|
||||
<div class="rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-[11px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
|
||||
<div class="font-medium text-black dark:text-fg">Required Cloudflare permissions</div>
|
||||
<ul class="list-inside list-disc">
|
||||
<li>Zone - DNS - Edit</li>
|
||||
<li>Zone - Zone - Read</li>
|
||||
</ul>
|
||||
<p>Limit zone resources to the zones Coolify should manage.</p>
|
||||
<a href="https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22edit%22%7D%5D&accountId=%2A&zoneId=all&name=Coolify%20DNS%20Management"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="font-medium text-coollabs hover:underline dark:text-warning">
|
||||
Create this token in Cloudflare
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-end border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
|
||||
<x-forms.button type="submit" wire:target="addToken" isHighlighted>
|
||||
Validate and add
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Integration Tokens | Coolify
|
||||
</x-slot>
|
||||
|
||||
<x-security.settings-layout>
|
||||
<div class="application-settings-form">
|
||||
<x-application.settings-section title="Integration tokens"
|
||||
description="Credentials used by third-party integrations such as DNS providers." flush>
|
||||
<x-slot:actions>
|
||||
@can('create', App\Models\IntegrationToken::class)
|
||||
<x-modal-input title="New Integration Token">
|
||||
<x-slot:content>
|
||||
<button type="button" class="button button-highlighted">
|
||||
<x-reicon name="plus" class="size-3.5" />
|
||||
New token
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<livewire:security.integration-token-form :modal_mode="true"
|
||||
wire:key="new-integration-token" />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
</x-slot:actions>
|
||||
|
||||
@if ($tokens->isEmpty())
|
||||
<x-empty title="No integration tokens"
|
||||
description="Add a provider token to connect a third-party integration."
|
||||
icon-name="keys" size="sm" />
|
||||
@else
|
||||
<div class="divide-y divide-neutral-200 dark:divide-white/[0.07]">
|
||||
@foreach ($tokens as $savedToken)
|
||||
<div wire:key="integration-token-{{ $savedToken->id }}"
|
||||
x-data="{
|
||||
visible: true,
|
||||
tokenName: @js($savedToken->name),
|
||||
tokenCapabilities: @js($savedToken->capabilities),
|
||||
}"
|
||||
x-show="visible"
|
||||
x-on:integration-token-updated.window="
|
||||
if ($event.detail.uuid === @js($savedToken->uuid)) {
|
||||
tokenName = $event.detail.name;
|
||||
tokenCapabilities = $event.detail.capabilities;
|
||||
}
|
||||
"
|
||||
x-on:integration-token-deleted.window="
|
||||
if ($event.detail.uuid === @js($savedToken->uuid)) visible = false
|
||||
">
|
||||
<x-modal-input title="Edit Integration Token" isFullWidth :wireIgnore="false"
|
||||
:contentClicks="false"
|
||||
class="border-b border-neutral-200 last:border-b-0 dark:border-white/[0.07]">
|
||||
<x-slot:content>
|
||||
<div class="grid min-h-14 w-full grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_2rem] items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]">
|
||||
<div class="min-w-0">
|
||||
<h3 class="truncate text-[13px]! font-semibold! text-black dark:text-fg">
|
||||
<span x-text="tokenName"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="text-center text-[12px] text-neutral-500 dark:text-fg-dim">
|
||||
{{ ucfirst($savedToken->provider) }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="capability in tokenCapabilities" :key="capability">
|
||||
<span x-text="capability"
|
||||
class="rounded-full bg-neutral-100 px-2 py-0.5 text-[10px] font-medium uppercase text-neutral-600 dark:bg-white/[0.06] dark:text-fg-dim"></span>
|
||||
</template>
|
||||
</div>
|
||||
<button type="button" class="icon-button" title="Edit integration token"
|
||||
:aria-label="`Edit ${tokenName}`" @click="modalOpen=true">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</x-slot:content>
|
||||
<livewire:security.integration-token-editor
|
||||
:integration_token_uuid="$savedToken->uuid"
|
||||
:key="'integration-token-editor-'.$savedToken->uuid" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</x-application.settings-section>
|
||||
</div>
|
||||
</x-security.settings-layout>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="mb-1.5 text-xs font-medium text-neutral-500 dark:text-fg-dim">Read-only bind mount</p>
|
||||
<x-forms.copy-button
|
||||
<x-forms.copy-input
|
||||
text="- /data/coolify/ssl/coolify-ca.crt:/etc/ssl/certs/coolify-ca.crt:ro" />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
</x-slot:actions>
|
||||
|
||||
<x-callout type="info" title="Supported package managers">
|
||||
Automated package discovery currently supports apt, dnf, and zypper. Weekly status notifications
|
||||
can be managed from
|
||||
Automated package discovery currently supports apk, apt, dnf, pacman, and zypper. Weekly status
|
||||
notifications can be managed from
|
||||
<a class="font-medium underline" href="{{ route('notifications.email') }}"
|
||||
{{ wireNavigate() }}>notification settings</a>.
|
||||
</x-callout>
|
||||
|
||||
@@ -5,76 +5,126 @@
|
||||
|
||||
<x-settings.layout>
|
||||
<x-slot:submenu>
|
||||
<div
|
||||
x-data="{ activeProvider: location.hash.slice(1).replace('-oauth-section', '') || '{{ $oauth_settings_map[0]['provider'] ?? '' }}' }"
|
||||
@hashchange.window="activeProvider = location.hash.slice(1).replace('-oauth-section', '')">
|
||||
<nav aria-label="OAuth providers"
|
||||
class="grid gap-0.5 py-1">
|
||||
@foreach ($oauth_settings_map as $oauth_setting)
|
||||
@php
|
||||
$provider = $oauth_setting['provider'];
|
||||
$providerLabel = str($provider)->headline();
|
||||
@endphp
|
||||
<a href="#{{ $provider }}-oauth-section" class="menu-item min-h-8! py-1! text-[12px]!"
|
||||
:class="{ 'menu-item-active': activeProvider === '{{ $provider }}' }"
|
||||
@click.prevent="activeProvider = '{{ $provider }}'; history.replaceState(null, '', '#{{ $provider }}-oauth-section'); window.scrollToSettingsSection?.('{{ $provider }}-oauth-section')">
|
||||
<span class="menu-item-icon bg-current"
|
||||
style="mask: url('{{ asset('svgs/' . $provider . '.svg') }}') center / contain no-repeat; -webkit-mask: url('{{ asset('svgs/' . $provider . '.svg') }}') center / contain no-repeat;"></span>
|
||||
<span class="menu-item-label">{{ $providerLabel }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
<div
|
||||
x-data="{ activeProvider: location.hash.slice(1).replace('-oauth-section', '') || @js($selectedProvider ?? array_key_first($oauth_settings_map)) }"
|
||||
@hashchange.window="activeProvider = location.hash.slice(1).replace('-oauth-section', '')">
|
||||
<nav aria-label="OAuth providers" class="grid gap-0.5 py-1">
|
||||
@foreach ($oauth_settings_map as $provider => $oauth_setting)
|
||||
<a href="#{{ $provider }}-oauth-section" class="menu-item min-h-8! py-1! text-[12px]!"
|
||||
:class="{ 'menu-item-active': activeProvider === '{{ $provider }}' }"
|
||||
@click.prevent="activeProvider = '{{ $provider }}'; history.replaceState(null, '', '#{{ $provider }}-oauth-section'); window.scrollToSettingsSection?.('{{ $provider }}-oauth-section')">
|
||||
<span class="menu-item-icon bg-current"
|
||||
style="mask: url('{{ asset('svgs/' . $provider . '.svg') }}') center / contain no-repeat; -webkit-mask: url('{{ asset('svgs/' . $provider . '.svg') }}') center / contain no-repeat;"></span>
|
||||
<span class="menu-item-label">{{ $oauth_setting['label'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
</x-slot:submenu>
|
||||
|
||||
<form wire:submit="submit" class="application-settings-form flex w-full min-w-0 flex-col gap-6">
|
||||
<x-unsaved-bar action="submit" />
|
||||
@foreach ($oauth_settings_map as $oauth_setting)
|
||||
@php
|
||||
$provider = $oauth_setting['provider'];
|
||||
$providerLabel = str($provider)->headline();
|
||||
@endphp
|
||||
|
||||
<x-application.settings-section title="Registration"
|
||||
description="Control password registration when an OAuth provider is available.">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="disable_registration_when_oauth_enabled"
|
||||
label="Disable password registration when OAuth is enabled"
|
||||
helper="OAuth providers can still create users when registration is enabled for that provider."
|
||||
instantSave="saveRegistrationPolicy" />
|
||||
</x-application.settings-section>
|
||||
|
||||
@foreach ($oauth_settings_map as $provider => $oauth_setting)
|
||||
<x-application.settings-section id="{{ $provider }}-oauth-section" class="scroll-mt-28"
|
||||
title="{{ $providerLabel }}">
|
||||
title="{{ $oauth_setting['label'] }}">
|
||||
<x-slot:actions>
|
||||
<div x-data="{ enabled: @js((bool) $oauth_setting['enabled']), provider: @js($provider) }">
|
||||
<x-forms.button type="button" :isHighlighted="!$oauth_setting['enabled']"
|
||||
<x-forms.button canGate="update" :canResource="$settings" type="button"
|
||||
:isHighlighted="!$oauth_setting['enabled']"
|
||||
x-on:click="
|
||||
if (!enabled) {
|
||||
const invalidField = [...$el.closest('section').querySelectorAll('[required]')]
|
||||
.find(field => !field.checkValidity());
|
||||
if (invalidField) { invalidField.reportValidity(); return; }
|
||||
}
|
||||
$wire.toggleProvider(provider);
|
||||
">
|
||||
if (!enabled) {
|
||||
const invalidField = [...$el.closest('section').querySelectorAll('[required]')]
|
||||
.find(field => !field.checkValidity());
|
||||
if (invalidField) { invalidField.reportValidity(); return; }
|
||||
}
|
||||
$wire.toggleProvider(provider);
|
||||
">
|
||||
{{ $oauth_setting['enabled'] ? 'Disable' : 'Enable' }}
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</x-slot:actions>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input id="oauth_settings_map.{{ $provider }}.redirect_uri"
|
||||
placeholder="{{ route('auth.callback', $provider) }}" label="Redirect URI" />
|
||||
|
||||
<x-forms.input id="oauth_settings_map.{{ $provider }}.client_id"
|
||||
label="Client ID" required />
|
||||
<x-forms.input id="oauth_settings_map.{{ $provider }}.client_secret"
|
||||
type="password" label="Client secret" autocomplete="new-password" required />
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
@if ($provider === 'oidc')
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.redirect_uri"
|
||||
placeholder="{{ route('auth.callback', $provider) }}" label="Redirect URI" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.base_url" label="Issuer URL" required
|
||||
helper="OpenID Provider issuer URL, for example https://example.okta.com. Coolify uses it to discover the authorization, token, userinfo, and JWKS endpoints." />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.client_id" label="Client ID" required />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.client_secret" type="password"
|
||||
label="Client secret" autocomplete="new-password" required />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.scopes" label="Scopes"
|
||||
helper="Must include openid. Common scopes are openid email profile groups." />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.clock_skew_seconds" type="number"
|
||||
label="Clock skew (seconds)" />
|
||||
<div class="lg:col-span-2">
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.custom_label" label="Login button label"
|
||||
placeholder="Login with SSO" />
|
||||
</div>
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.redirect_uri"
|
||||
placeholder="{{ route('auth.callback', $provider) }}" label="Redirect URI" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.client_id" label="Client ID" required />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.client_secret" type="password"
|
||||
label="Client secret" autocomplete="new-password" required />
|
||||
@endif
|
||||
|
||||
@if ($provider === 'azure')
|
||||
<x-forms.input id="oauth_settings_map.{{ $provider }}.tenant"
|
||||
label="Tenant" required />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.tenant" label="Tenant" required />
|
||||
@endif
|
||||
|
||||
@if ($provider === 'google')
|
||||
<x-forms.input id="oauth_settings_map.{{ $provider }}.tenant"
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.tenant"
|
||||
helper="Optional hosted domain supplied to Google as a login hint."
|
||||
label="Hosted domain" />
|
||||
@endif
|
||||
|
||||
@if (in_array($provider, ['authentik', 'clerk', 'zitadel', 'gitlab'], true))
|
||||
<x-forms.input id="oauth_settings_map.{{ $provider }}.base_url"
|
||||
label="Base URL" :required="in_array($provider, ['authentik', 'clerk'], true)" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.base_url" label="Base URL"
|
||||
:required="in_array($provider, ['authentik', 'clerk'], true)" />
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 lg:grid-cols-2">
|
||||
@if ($provider === 'oidc')
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.allow_registration"
|
||||
label="Allow OIDC user creation"
|
||||
helper="Allow a successful OIDC login to create a user when password registration is disabled." />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.require_email_verified"
|
||||
label="Require verified email" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.use_pkce" label="Use PKCE" />
|
||||
@endif
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.auto_join_root_team"
|
||||
label="Auto-join new users to Root team"
|
||||
helper="Add newly-created OAuth users to the Root team as members without creating a personal team." />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
@endforeach
|
||||
|
||||
@@ -13,12 +13,19 @@
|
||||
|
||||
<x-application.settings-section id="access-section" title="Access">
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.listbox id="is_registration_enabled" label="Registration"
|
||||
<x-forms.listbox id="is_registration_enabled" label="Registration"
|
||||
helper="Allow users to create their own account. When disabled, only administrators can create accounts."
|
||||
onChange="instantSave" :options="[
|
||||
['value' => true, 'label' => 'Anyone can register'],
|
||||
['value' => false, 'label' => 'Registration disabled'],
|
||||
]" />
|
||||
]" />
|
||||
<x-forms.listbox canGate="update" :canResource="$settings"
|
||||
id="disable_registration_when_oauth_enabled" label="Password registration with OAuth"
|
||||
helper="Hide password registration whenever at least one OAuth provider is enabled."
|
||||
onChange="instantSave" :options="[
|
||||
['value' => false, 'label' => 'Allow password registration'],
|
||||
['value' => true, 'label' => 'Disable when OAuth is enabled'],
|
||||
]" />
|
||||
<x-forms.listbox id="disable_two_step_confirmation" label="Destructive action confirmation"
|
||||
helper="Choose whether destructive actions require password and text confirmation."
|
||||
onChange="instantSave" :options="[
|
||||
|
||||
@@ -29,14 +29,7 @@
|
||||
<span
|
||||
class="min-w-0 truncate font-mono text-[12px] text-neutral-500 dark:text-fg-dim"
|
||||
title="{{ $invite->link }}">{{ $invite->link }}</span>
|
||||
<button type="button"
|
||||
class="button h-7! shrink-0 px-2!"
|
||||
title="Copy invitation link"
|
||||
aria-label="Copy invitation link"
|
||||
x-data
|
||||
x-on:click.prevent="window.copyToClipboard(@js($invite->link))">
|
||||
<x-reicon name="file-content" class="size-3.5" />
|
||||
</button>
|
||||
<x-copy-button :value="$invite->link" label="Copy invitation link" />
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="button"
|
||||
|
||||
Reference in New Issue
Block a user