Feat: refactor app role duration to validity date

- Replaced `duration` (integer, days) with `validUpto` (datetime) in role-app assignments.
- Updated `AssignAppsToRoleData`, `AppRole` model, and Livewire forms to adopt the new `validUpto` field.
- Adjusted database migrations and UI components to reflect the change in validity representation.
- Enhanced validity date inputs with support for "Unlimited" roles using toggles.
This commit is contained in:
= 2026-05-21 06:38:05 +00:00
parent 1e5c084855
commit fd57ac57e1
9 changed files with 68 additions and 62 deletions

View File

@ -10,7 +10,7 @@ final class AssignAppsToRoleData extends Data
{
public function __construct(
public int $roleId,
public int $duration,
public string $validUpto,
/** @var int[] */
public array $appIds
) {}

View File

@ -13,7 +13,7 @@ public function __construct(
public int $id,
public int $appId,
public string $appName,
public int $duration,
public string $duration,
) {}
/**

View File

@ -15,13 +15,15 @@ class AssignAppToRoleForm extends Form
])]
public array $appIds = [];
#[Validate('required|integer|min:1|max:3650')]
public int $duration = 30;
#[Validate('exclude_if:isUnlimited,true|required|date|after_or_equal:today')]
public ?string $validUpto = null;
public bool $isUnlimited = false;
public function reset(mixed ...$properties): void
{
$this->resetErrorBag();
parent::reset(...$properties);
$this->duration = 30;
$this->validUpto = null;
}
}

View File

@ -12,7 +12,7 @@
* @property int $id
* @property int $app_id
* @property int $role_id
* @property int $duration Duration in days.
* @property string $duration Validity of the role
*/
#[Fillable(['app_id', 'role_id', 'duration'])]
class AppRole extends Pivot

View File

@ -30,6 +30,7 @@ public function assign(AssignAppsToRoleData $data): void
if (empty($data->appIds)) {
return;
}
DB::transaction(function () use ($data): void {
$role = Role::query()->findOrFail($data->roleId);
$this->assignAppPermissionsRole($role, $data->appIds);
@ -37,7 +38,7 @@ public function assign(AssignAppsToRoleData $data): void
$records = array_map(fn (int $appId) => [
'role_id' => $data->roleId,
'app_id' => $appId,
'duration' => $data->duration,
'duration' => $data->validUpto,
], $data->appIds);
AppRole::query()->upsert(
@ -45,7 +46,7 @@ public function assign(AssignAppsToRoleData $data): void
['role_id', 'app_id'],
['duration']
);
});
}
/**

View File

@ -18,7 +18,7 @@ public function up(): void
$table->id();
$table->foreignIdFor(ConnectedApp::class, 'app_id');
$table->foreignId('role_id')->comment('role_id from spatie roles table');
$table->integer('duration')->comment('in days');
$table->dateTime('duration');
$table->unique(['app_id', 'role_id']);
});
}

View File

@ -81,13 +81,11 @@ public function save(): void
$this->attempt(
action: function (): void {
$role = $this->roleService->create(new CreateRoleData(name: $this->form->name));
$this->appRoleService->assign(
new AssignAppsToRoleData(
$this->appRoleService->assign(new AssignAppsToRoleData(
roleId: $role->id,
duration: $this->form->duration,
appIds: $this->form->appIds
)
);
validUpto: $this->form->isUnlimited ? now()->addYears(15) : $this->form->validUpto,
appIds: $this->form->appIds,
));
$this->showCreateModal = false;
$this->form->reset();
unset($this->getRoles);

View File

@ -7,6 +7,7 @@
use App\Models\Role;
use App\Services\AppRoleService;
use Livewire\Attributes\{Computed, Layout, Title};
use Illuminate\Support\Carbon;
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
@ -71,7 +72,7 @@ public function mount(int $id): void
$this->appHeaders = TableHeader::collect([
new TableHeader(key: 'app_name', label: 'App'),
new TableHeader(key: 'duration', label: 'Duration (days)'),
new TableHeader(key: 'duration', label: 'Validity'),
], DataCollection::class);
$this->userHeaders = TableHeader::collect([
@ -118,7 +119,7 @@ public function openEditAppModal(int $assignmentId): void
if ($assignment) {
$this->assignForm->appIds = [$assignment['appId'] ?? $assignment['id']];
$this->assignForm->duration = $assignment['duration'];
$this->assignForm->validUpto = $assignment['duration'];
$this->editingAppName = $assignment['appName'];
}
@ -160,12 +161,12 @@ public function assignApp(): void
{
$this->attempt(
action: function (): void {
$this->assignForm->validate();
$this->appRoleService->assign(new AssignAppsToRoleData(
roleId: $this->roleId,
duration: $this->assignForm->duration,
validUpto: $this->assignForm->isUnlimited ? now()->addYears(15) : $this->assignForm->validUpto,
appIds: $this->assignForm->appIds,
));
$this->assignForm->reset();
$this->showAddAppModal = false;
unset($this->apps);
@ -178,9 +179,10 @@ public function updateApp(): void
{
$this->attempt(
action: function (): void {
$this->assignForm->validate();
$this->appRoleService->assign(new AssignAppsToRoleData(
roleId: $this->roleId,
duration: $this->assignForm->duration,
validUpto: $this->assignForm->isUnlimited ? now()->addYears(15) : $this->assignForm->validUpto,
appIds: $this->assignForm->appIds,
));
@ -275,7 +277,7 @@ class="btn-sm btn-ghost"
<x-shared.card title="Connected Apps" icon="lucide-blocks" class="pb-2 w-full h-min">
<x-slot:actions>
<x-mary-button wire:click="openAddAppModal" icon="lucide.plus" class="btn-sm">
<x-mary-button wire:click="openAddAppModal" spinner="openAddAppModal" icon="lucide.plus" class="btn-sm">
Add App
</x-mary-button>
</x-slot:actions>
@ -290,7 +292,7 @@ class="btn-sm btn-ghost"
@endscope
@scope('cell_duration', $row)
{{ $row['duration'] }} days
{{ Carbon::parse($row['duration'])->diffForHumans() }}
@endscope
@scope('actions', $row)
@ -358,27 +360,16 @@ class="btn-sm btn-error btn-soft btn-circle"
@if(!$isEditingApp)
<div>
<div class="mb-1 flex items-end justify-between">
<label class="text-sm font-semibold text-gray-700">Connected Apps</label>
<div class="flex gap-3 text-xs">
<button type="button" wire:click="selectAllApps" class="text-primary hover:underline">
Select All
</button>
<button type="button" wire:click="clearApps" class="text-error hover:underline">
Clear
</button>
</div>
</div>
<x-mary-choices
<x-shared.choices
wire:model="assignForm.appIds"
:label="__('Connected Apps')"
:options="$appsSearchable"
searchable
search-function="searchApps"
option-label="name"
option-value="id"
select-all-action="selectAllApps"
clear-action="clearApps"
no-result-text="No apps found."
placeholder="Search apps..."
searchable
/>
</div>
@else
@ -390,23 +381,34 @@ class="btn-sm btn-error btn-soft btn-circle"
class="bg-gray-50 text-gray-500 cursor-not-allowed"
/>
@endif
<div x-data="{ isUnlimited: @entangle('assignForm.isUnlimited') }"
class="flex gap-2 items-baseline-last">
<x-mary-input
label="Duration (days)"
wire:model="assignForm.duration"
id="assign-duration-input"
type="number"
min="1"
max="3650"
placeholder="30"
<div class="flex-1 transition-opacity duration-200"
x-bind:class="isUnlimited ? 'opacity-50 pointer-events-none' : ''">
<x-mary-datepicker
label="Date"
wire:model="assignForm.validUpto"
icon="lucide.calendar"
x-bind:disabled="isUnlimited"
x-bind:readonly="isUnlimited"
/>
</div>
<x-mary-toggle
class="toggle-secondary"
label="Unlimited"
x-model="isUnlimited"
/>
</div>
</div>
<x-slot:actions>
<x-mary-button @click="$wire.showAddAppModal = false" class="btn-ghost">
Cancel
</x-mary-button>
<x-mary-button type="submit" icon="{{ $isEditingApp ? 'lucide.save' : 'lucide.link' }}"
<x-mary-button type="submit" class="btn-primary"
icon="{{ $isEditingApp ? 'lucide.save' : 'lucide.link' }}"
spinner="saveApp">
{{ $isEditingApp ? 'Update Duration' : 'Assign Apps' }}
</x-mary-button>

View File

@ -1,5 +1,5 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>
{{ filled($title ?? null) ? $title.' - '.config('app.name', 'Laravel') : config('app.name', 'Laravel') }}
@ -13,3 +13,6 @@
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
{{--For date picker in maryui. Remove this if opt for native --}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>