mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-16 22:14:34 +00:00
refactor(auth): enforce team member authorization across app
Restrict sensitive operations to admins/owners and hide sensitive data from team members: - Add authorization checks to Livewire components and API endpoints - Restrict team members from accessing sensitive permissions and data - Hide environment variable values from non-admin team members - Update policies to enforce team-level admin status requirement - Add useSensitivePermissions policy for read:sensitive tokens - Improve disabled button UX with auth-specific tooltips - Add authorization checks in middleware for API tokens Closes authorization gaps in project management, server management, and settings components.
This commit is contained in:
1 parent
e82942b387
commit
b878dc8102
49 files changed
+919
-136
No files matched your search
@@ -122,7 +122,11 @@
|
||||
}
|
||||
|
||||
@utility button {
|
||||
@apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-transparent disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base;
|
||||
@apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-neutral-200 dark:disabled:border-coolgray-300 disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base;
|
||||
}
|
||||
|
||||
@utility auth-tooltip {
|
||||
@apply fixed z-[99] px-2.5 py-1.5 text-xs rounded-sm pointer-events-none whitespace-nowrap text-neutral-700 bg-neutral-200 dark:text-neutral-300 dark:bg-coolgray-400;
|
||||
}
|
||||
|
||||
@utility alert-success {
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
@if ($authDisabled)
|
||||
<span class="relative inline-flex"
|
||||
x-data="{ visible: false, _t: null }"
|
||||
@mouseenter="_t = setTimeout(() => {
|
||||
visible = true;
|
||||
$nextTick(() => requestAnimationFrame(() => {
|
||||
const tip = $refs.tip;
|
||||
if (!tip) return;
|
||||
const r = $el.getBoundingClientRect();
|
||||
const t = tip.getBoundingClientRect();
|
||||
let top = r.top - t.height - 6;
|
||||
let left = r.left;
|
||||
if (top < 4) top = r.bottom + 6;
|
||||
if (left + t.width > innerWidth - 8) left = innerWidth - 8 - t.width;
|
||||
if (left < 4) left = 4;
|
||||
tip.style.top = top + 'px';
|
||||
tip.style.left = left + 'px';
|
||||
}));
|
||||
}, 300)"
|
||||
@mouseleave="clearTimeout(_t); visible = false">
|
||||
@endif
|
||||
<button @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
{{ $attributes->merge(['type' => 'button']) }}
|
||||
@isset($confirm)
|
||||
@@ -18,3 +39,9 @@
|
||||
@endif
|
||||
@endif
|
||||
</button>
|
||||
@if ($authDisabled)
|
||||
<div x-ref="tip" x-show="visible" x-cloak class="auth-tooltip">
|
||||
You do not have permission to perform this action.
|
||||
</div>
|
||||
</span>
|
||||
@endif
|
||||
@@ -6,6 +6,7 @@
|
||||
'buttonFullWidth' => false,
|
||||
'customButton' => null,
|
||||
'disabled' => false,
|
||||
'authDisabled' => false,
|
||||
'dispatchAction' => false,
|
||||
'submitAction' => 'delete',
|
||||
'content' => null,
|
||||
@@ -145,11 +146,11 @@
|
||||
@else
|
||||
@if ($disabled)
|
||||
@if ($buttonFullWidth)
|
||||
<x-forms.button class="w-full" isError disabled wire:target>
|
||||
<x-forms.button class="w-full" isError disabled :authDisabled="$authDisabled" wire:target>
|
||||
{{ $buttonTitle }}
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button isError disabled wire:target>
|
||||
<x-forms.button isError disabled :authDisabled="$authDisabled" wire:target>
|
||||
{{ $buttonTitle }}
|
||||
</x-forms.button>
|
||||
@endif
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
class="block w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-coolgray-300">
|
||||
Download displayed logs
|
||||
</button>
|
||||
@can('update', $application)
|
||||
<button x-on:click="
|
||||
downloadingAllLogs = true;
|
||||
$wire.downloadAllLogs().then(logs => {
|
||||
@@ -276,6 +277,7 @@
|
||||
Downloading...
|
||||
</span>
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('deploy', $application)" title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('deploy', $application)" :authDisabled="!auth()->user()->can('deploy', $application)" title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
@if ($database->destination->server->isFunctional())
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('manage', $database)" title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('manage', $database)" :authDisabled="!auth()->user()->can('manage', $database)" title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
@@ -58,7 +58,7 @@
|
||||
Restart
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('manage', $database)" title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('manage', $database)" :authDisabled="!auth()->user()->can('manage', $database)" title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
@@ -80,7 +80,7 @@
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<button @disabled(!auth()->user()->can('manage', $database)) @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<x-forms.button canGate="manage" :canResource="$database" @click="$wire.dispatch('startEvent')" class="gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
@@ -88,17 +88,15 @@
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Start
|
||||
</button>
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@script
|
||||
<script>
|
||||
$wire.$on('startEvent', () => {
|
||||
window.dispatchEvent(new CustomEvent('startdatabase'));
|
||||
$wire.$call('start');
|
||||
});
|
||||
$wire.$on('restartEvent', () => {
|
||||
$wire.$dispatch('info', 'Restarting database.');
|
||||
window.dispatchEvent(new CustomEvent('startdatabase'));
|
||||
$wire.$call('restart');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('stop', $service)" title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('stop', $service)" :authDisabled="!auth()->user()->can('stop', $service)" title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
@@ -68,7 +68,7 @@
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('stop', $service)" title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('stop', $service)" :authDisabled="!auth()->user()->can('stop', $service)" title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
@@ -86,7 +86,7 @@
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<button @disabled(!auth()->user()->can('deploy', $service)) @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<x-forms.button canGate="deploy" :canResource="$service" @click="$wire.dispatch('startEvent')" class="gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
@@ -94,9 +94,9 @@
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('stop', $service)" title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('stop', $service)" :authDisabled="!auth()->user()->can('stop', $service)" title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
@@ -113,7 +113,7 @@
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<button @disabled(!auth()->user()->can('deploy', $service)) @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<x-forms.button canGate="deploy" :canResource="$service" @click="$wire.dispatch('startEvent')" class="gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
@@ -121,7 +121,7 @@
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
@@ -149,11 +149,9 @@
|
||||
);
|
||||
return;
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('startservice'));
|
||||
$wire.$call('start');
|
||||
});
|
||||
$wire.$on('forceDeployEvent', () => {
|
||||
window.dispatchEvent(new CustomEvent('startservice'));
|
||||
$wire.$call('forceDeploy');
|
||||
});
|
||||
$wire.$on('restartEvent', async () => {
|
||||
@@ -166,12 +164,10 @@
|
||||
}
|
||||
$wire.$dispatch('info',
|
||||
'Gracefully stopping service.<br/><br/>It could take a while depending on the service.');
|
||||
window.dispatchEvent(new CustomEvent('startservice'));
|
||||
$wire.$call('restart');
|
||||
});
|
||||
$wire.$on('pullAndRestartEvent', () => {
|
||||
$wire.$dispatch('info', 'Pulling new images and restarting service.');
|
||||
window.dispatchEvent(new CustomEvent('startservice'));
|
||||
$wire.$call('pullAndRestartEvent');
|
||||
});
|
||||
$wire.on('imagePulled', () => {
|
||||
|
||||
@@ -139,15 +139,22 @@
|
||||
@else
|
||||
<div class="flex flex-col w-full gap-2 lg:flex-row">
|
||||
<x-forms.input disabled id="key" />
|
||||
<x-forms.env-var-input
|
||||
disabled
|
||||
type="password"
|
||||
id="value"
|
||||
:availableVars="$this->availableSharedVariables"
|
||||
:projectUuid="data_get($parameters, 'project_uuid')"
|
||||
:environmentUuid="data_get($parameters, 'environment_uuid')" />
|
||||
@if ($is_shared)
|
||||
<x-forms.input disabled type="password" id="real_value" />
|
||||
@if ($isValueHidden)
|
||||
<div class="w-full">
|
||||
<input disabled type="text" value="Hidden (only admins can view)"
|
||||
class="input italic !text-neutral-500 dark:!text-neutral-500" />
|
||||
</div>
|
||||
@else
|
||||
<x-forms.env-var-input
|
||||
disabled
|
||||
type="password"
|
||||
id="value"
|
||||
:availableVars="$this->availableSharedVariables"
|
||||
:projectUuid="data_get($parameters, 'project_uuid')"
|
||||
:environmentUuid="data_get($parameters, 'environment_uuid')" />
|
||||
@if ($is_shared)
|
||||
<x-forms.input disabled type="password" id="real_value" />
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h2>Healthchecks</h2>
|
||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">Save</x-forms.button>
|
||||
@if (!$healthCheckEnabled)
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('update', $resource)" title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('update', $resource)" :authDisabled="!auth()->user()->can('update', $resource)" title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
||||
submitAction="toggleHealthcheck" :actions="['Enable healthcheck for this resource.']"
|
||||
warningMessage="If the health check fails, your application will become inaccessible. Please review the <a href='https://coolify.io/docs/knowledge-base/health-checks' target='_blank' class='underline text-white'>Health Checks</a> guide before proceeding!"
|
||||
step2ButtonText="Enable Healthcheck" :confirmWithText="false" :confirmWithPassword="false"
|
||||
|
||||
@@ -13,36 +13,43 @@
|
||||
<div class="flex flex-col gap-2 pb-2">
|
||||
<div class="flex gap-2 items-end">
|
||||
<h2>Scheduled Task</h2>
|
||||
<x-forms.button type="submit">
|
||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
@if ($resource->isRunning())
|
||||
<x-forms.button type="button" wire:click="executeNow">
|
||||
Execute Now
|
||||
</x-forms.button>
|
||||
@can('update', $resource)
|
||||
<x-forms.button type="button" wire:click="executeNow">
|
||||
Execute Now
|
||||
</x-forms.button>
|
||||
@endcan
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Scheduled Task Deletion?" isErrorButton buttonTitle="Delete"
|
||||
submitAction="delete({{ $task->id }})" :actions="['The selected scheduled task will be permanently deleted.']" confirmationText="{{ $task->name }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Scheduled Task Name below"
|
||||
shortConfirmationLabel="Scheduled Task Name" :confirmWithPassword="false"
|
||||
step2ButtonText="Permanently Delete" />
|
||||
|
||||
@can('update', $resource)
|
||||
<x-modal-confirmation title="Confirm Scheduled Task Deletion?" isErrorButton buttonTitle="Delete"
|
||||
submitAction="delete({{ $task->id }})" :actions="['The selected scheduled task will be permanently deleted.']" confirmationText="{{ $task->name }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Scheduled Task Name below"
|
||||
shortConfirmationLabel="Scheduled Task Name" :confirmWithPassword="false"
|
||||
step2ButtonText="Permanently Delete" />
|
||||
@endcan
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox instantSave id="isEnabled" label="Enabled" />
|
||||
@can('update', $resource)
|
||||
<x-forms.checkbox instantSave id="isEnabled" label="Enabled" />
|
||||
@else
|
||||
<x-forms.checkbox disabled id="isEnabled" label="Enabled" />
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex gap-2 w-full">
|
||||
<x-forms.input placeholder="Name" id="name" label="Name" required />
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
|
||||
<x-forms.input type="number" placeholder="300" id="timeout"
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="Name" id="name" label="Name" required />
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" type="number" placeholder="300" id="timeout"
|
||||
helper="Maximum execution time in seconds (60-36000)." label="Timeout (seconds)" required />
|
||||
@if ($type === 'application')
|
||||
<x-forms.input placeholder="php"
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="php"
|
||||
helper="You can leave this empty if your resource only has one container." id="container"
|
||||
label="Container name" />
|
||||
@elseif ($type === 'service')
|
||||
<x-forms.input placeholder="php"
|
||||
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="php"
|
||||
helper="You can leave this empty if your resource only has one service in your stack. Otherwise use the stack name, without the random generated ID. So if you have a mysql service in your stack, use mysql."
|
||||
id="container" label="Service name" />
|
||||
@endif
|
||||
|
||||
@@ -18,21 +18,23 @@
|
||||
<x-forms.input required id="description" label="Description" />
|
||||
<x-forms.button type="submit">Create</x-forms.button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
Permissions
|
||||
<x-helper class="px-1" helper="These permissions will be granted to the token." /><span
|
||||
class="pr-1">:</span>
|
||||
<div class="flex gap-1 font-bold dark:text-white">
|
||||
@if ($permissions)
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Permissions</span>
|
||||
<x-helper helper="These permissions will be granted to the token." />
|
||||
@if ($permissions)
|
||||
<div class="flex gap-1.5 flex-wrap">
|
||||
@foreach ($permissions as $permission)
|
||||
<div>{{ $permission }}</div>
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs rounded-sm font-medium {{ $permission === 'root' ? 'bg-red-500/20 text-red-400' : ($permission === 'write' || $permission === 'write:sensitive' ? 'bg-amber-500/20 text-amber-400' : ($permission === 'deploy' ? 'bg-blue-500/20 text-blue-400' : 'bg-neutral-500/20 text-neutral-300')) }}">
|
||||
{{ $permission }}
|
||||
</span>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h4>Token Permissions</h4>
|
||||
<div class="w-64">
|
||||
<div class="w-96">
|
||||
@if ($canUseRootPermissions)
|
||||
<x-forms.checkbox label="root" wire:model.live="permissions" domValue="root"
|
||||
helper="Root access, be careful!" :checked="in_array('root', $permissions)"></x-forms.checkbox>
|
||||
@@ -59,9 +61,14 @@
|
||||
@endif
|
||||
<x-forms.checkbox label="read" domValue="read" wire:model.live="permissions" domValue="read"
|
||||
:checked="in_array('read', $permissions)"></x-forms.checkbox>
|
||||
<x-forms.checkbox label="read:sensitive" wire:model.live="permissions" domValue="read:sensitive"
|
||||
helper="Responses will include secrets, logs, passwords, and compose file contents."
|
||||
:checked="in_array('read:sensitive', $permissions)"></x-forms.checkbox>
|
||||
@if ($canUseSensitivePermissions)
|
||||
<x-forms.checkbox label="read:sensitive" wire:model.live="permissions" domValue="read:sensitive"
|
||||
helper="Responses will include secrets, logs, passwords, and compose file contents."
|
||||
:checked="in_array('read:sensitive', $permissions)"></x-forms.checkbox>
|
||||
@else
|
||||
<x-forms.checkbox label="read:sensitive (admin/owner only)" disabled domValue="read:sensitive"
|
||||
helper="Read:sensitive access requires admin or owner role" :checked="false"></x-forms.checkbox>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@if (in_array('root', $permissions))
|
||||
@@ -70,44 +77,74 @@
|
||||
</form>
|
||||
@endcan
|
||||
@if (session()->has('token'))
|
||||
<div class="py-4 font-bold dark:text-warning">Please copy this token now. For your security, it won't be shown
|
||||
again.
|
||||
<div class="p-4 my-4 border rounded dark:border-coolgray-200 dark:bg-coolgray-100">
|
||||
<div class="pb-2 font-bold dark:text-warning">Please copy this token now. For your security, it won't
|
||||
be shown again.</div>
|
||||
<div class="relative" x-data="{ copied: false, isSecure: window.isSecureContext }">
|
||||
<input type="text" value="{{ session('token') }}" readonly
|
||||
class="input !text-white !bg-coolgray-200 font-mono" />
|
||||
<button x-show="isSecure"
|
||||
@click.prevent="copied = true; navigator.clipboard.writeText({{ Js::from(session('token')) }}); setTimeout(() => copied = false, 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 x-show="!copied" 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>
|
||||
<svg x-show="copied" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pb-4 font-bold dark:text-white"> {{ session('token') }}</div>
|
||||
@endif
|
||||
<h3 class="py-4">Issued Tokens</h3>
|
||||
<div class="grid gap-2 lg:grid-cols-1">
|
||||
@forelse ($tokens as $token)
|
||||
<div wire:key="token-{{ $token->id }}"
|
||||
class="flex flex-col gap-1 p-2 border dark:border-coolgray-200 hover:no-underline">
|
||||
<div>Description: {{ $token->name }}</div>
|
||||
<div>Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}</div>
|
||||
<div class="flex gap-1">
|
||||
@if ($token->abilities)
|
||||
Permissions:
|
||||
@foreach ($token->abilities as $ability)
|
||||
<div class="font-bold dark:text-white">{{ $ability }}</div>
|
||||
@endforeach
|
||||
<div x-data="{ search: '' }">
|
||||
<div class="flex items-center justify-between py-4">
|
||||
<h3>Issued Tokens</h3>
|
||||
@if ($tokens->count() > 1)
|
||||
<input type="text" x-model="search" placeholder="Filter tokens..."
|
||||
class="input w-64" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@forelse ($tokens as $token)
|
||||
<div wire:key="token-{{ $token->id }}"
|
||||
x-show="!search || '{{ strtolower($token->name) }} {{ strtolower(implode(' ', $token->abilities ?? [])) }}'.includes(search.toLowerCase())"
|
||||
class="flex items-center justify-between p-3 border rounded dark:border-coolgray-200">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold dark:text-white">{{ $token->name }}</span>
|
||||
@if ($token->abilities)
|
||||
@foreach ($token->abilities as $ability)
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs rounded-sm font-medium {{ $ability === 'root' ? 'bg-red-500/20 text-red-400' : ($ability === 'write' || $ability === 'write:sensitive' ? 'bg-amber-500/20 text-amber-400' : ($ability === 'deploy' ? 'bg-blue-500/20 text-blue-400' : 'bg-neutral-500/20 text-neutral-300')) }}">
|
||||
{{ $ability }}
|
||||
</span>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-xs text-neutral-400">
|
||||
Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}
|
||||
</div>
|
||||
</div>
|
||||
@if (auth()->id() === $token->tokenable_id)
|
||||
<x-modal-confirmation title="Confirm API Token Revocation?" isErrorButton
|
||||
buttonTitle="Revoke" submitAction="revoke({{ data_get($token, 'id') }})"
|
||||
:actions="[
|
||||
'This API Token will be revoked and permanently deleted.',
|
||||
'Any API call made with this token will fail.',
|
||||
]" confirmationText="{{ $token->name }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the API Token Description below"
|
||||
shortConfirmationLabel="API Token Description" :confirmWithPassword="false"
|
||||
step2ButtonText="Revoke API Token" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (auth()->id() === $token->tokenable_id)
|
||||
<x-modal-confirmation title="Confirm API Token Revocation?" isErrorButton buttonTitle="Revoke token"
|
||||
submitAction="revoke({{ data_get($token, 'id') }})" :actions="[
|
||||
'This API Token will be revoked and permanently deleted.',
|
||||
'Any API call made with this token will fail.',
|
||||
]"
|
||||
confirmationText="{{ $token->name }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the API Token Description below"
|
||||
shortConfirmationLabel="API Token Description" :confirmWithPassword="false"
|
||||
step2ButtonText="Revoke API Token" />
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div>
|
||||
<div>No API tokens found.</div>
|
||||
</div>
|
||||
@endforelse
|
||||
@empty
|
||||
<div class="text-neutral-400">No API tokens found.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
Reference in new issue
Block a user