feat(#20): preference: assign mirror device id to auto joined devices

This commit is contained in:
Benjamin Nussbaum 2025-05-08 22:49:25 +02:00
parent b4dfb1673f
commit 8d7a53b888
8 changed files with 138 additions and 2 deletions

View file

@ -25,6 +25,7 @@ class User extends Authenticatable // implements MustVerifyEmail
'email',
'password',
'assign_new_devices',
'assign_new_device_id'
];
/**

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->foreignId('assign_new_device_id')->nullable()->constrained('devices')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['assign_new_device_id']);
$table->dropColumn('assign_new_device_id');
});
}
};

View file

@ -66,7 +66,7 @@
<flux:menu.separator/>
<flux:menu.radio.group>
<flux:menu.item href="/settings/profile" wire:navigate icon="cog">Settings</flux:menu.item>
<flux:menu.item href="/settings/preferences" wire:navigate icon="cog">Settings</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator/>

View file

@ -1,6 +1,7 @@
<div class="flex items-start max-md:flex-col">
<div class="mr-10 w-full pb-4 md:w-[220px]">
<flux:navlist>
<flux:navlist.item href="{{ route('settings.preferences') }}" wire:navigate>Preferences</flux:navlist.item>
<flux:navlist.item href="{{ route('settings.profile') }}" wire:navigate>Profile</flux:navlist.item>
<flux:navlist.item href="{{ route('settings.password') }}" wire:navigate>Password</flux:navlist.item>
<flux:navlist.item href="{{ route('settings.appearance') }}" wire:navigate>Appearance</flux:navlist.item>

View file

@ -0,0 +1,63 @@
<?php
use App\Models\User;
use App\Models\Device;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\Rule;
use Livewire\Volt\Component;
new class extends Component {
public ?int $assign_new_device_id = null;
public function mount(): void
{
$this->assign_new_device_id = Auth::user()->assign_new_device_id;
}
public function updatePreferences(): void
{
$validated = $this->validate([
'assign_new_device_id' => [
'nullable',
Rule::exists('devices', 'id')->where(function ($query) {
$query->where('user_id', Auth::id())
->whereNull('mirror_device_id');
}),
],
]);
Auth::user()->update($validated);
$this->dispatch('profile-updated');
}
}; ?>
<section class="w-full">
@include('partials.settings-heading')
<x-settings.layout heading="Preferences" subheading="Update your preferences">
<form wire:submit="updatePreferences" class="my-6 w-full space-y-6">
<flux:select wire:model="assign_new_device_id" label="Auto-Joined Devices should mirror">
<flux:select.option value="">None</flux:select.option>
@foreach(auth()->user()->devices->where('mirror_device_id', null) as $device)
<flux:select.option value="{{ $device->id }}">
{{ $device->name }} ({{ $device->friendly_id }})
</flux:select.option>
@endforeach
</flux:select>
<div class="flex items-center gap-4">
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
</div>
<x-action-message class="me-3" on="profile-updated">
{{ __('Saved.') }}
</x-action-message>
</div>
</form>
</x-settings.layout>
</section>

View file

@ -28,6 +28,7 @@ Route::get('/display', function (Request $request) {
'name' => "{$auto_assign_user->name}'s TRMNL",
'friendly_id' => Str::random(6),
'default_refresh_interval' => 900,
'mirror_device_id' => $auto_assign_user->assign_new_device_id,
]);
} else {
return response()->json([
@ -135,6 +136,7 @@ Route::get('/setup', function (Request $request) {
'name' => "{$auto_assign_user->name}'s TRMNL",
'friendly_id' => Str::random(6),
'default_refresh_interval' => 900,
'mirror_device_id' => $auto_assign_user->assign_new_device_id,
]);
} else {
return response()->json([

View file

@ -8,7 +8,8 @@ Route::get('/', function () {
})->name('home');
Route::middleware(['auth'])->group(function () {
Route::redirect('settings', 'settings/profile');
Route::redirect('settings', 'settings/preferences');
Volt::route('settings/preferences', 'settings.preferences')->name('settings.preferences');
Volt::route('settings/profile', 'settings.profile')->name('settings.profile');
Volt::route('settings/password', 'settings.password')->name('settings.password');
Volt::route('settings/appearance', 'settings.appearance')->name('settings.appearance');

View file

@ -105,6 +105,45 @@ test('new device is auto-assigned to user with auto-assign enabled', function ()
->api_key->toBe('new-device-key');
});
test('new device is auto-assigned and mirrors specified device', function () {
// Create a source device that will be mirrored
$sourceDevice = Device::factory()->create([
'mac_address' => 'AA:BB:CC:DD:EE:FF',
'api_key' => 'source-api-key',
'current_screen_image' => 'source-image',
]);
// Create user with auto-assign enabled and mirror device set
$user = User::factory()->create([
'assign_new_devices' => true,
'assign_new_device_id' => $sourceDevice->id,
]);
// Make request from new device
$response = $this->withHeaders([
'id' => '00:11:22:33:44:55',
'access-token' => 'new-device-key',
'rssi' => -70,
'battery_voltage' => 3.8,
'fw-version' => '1.0.0',
])->get('/api/display');
$response->assertOk();
// Verify the new device was created and mirrors the source device
$newDevice = Device::where('mac_address', '00:11:22:33:44:55')->first();
expect($newDevice)
->not->toBeNull()
->user_id->toBe($user->id)
->api_key->toBe('new-device-key')
->mirror_device_id->toBe($sourceDevice->id);
// Verify the response contains the source device's image
$response->assertJson([
'filename' => 'source-image.bmp',
]);
});
test('device setup endpoint returns correct data', function () {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',