Skip to content

Commit 4907422

Browse files
committed
feat: implement user settings
1 parent 0321e8e commit 4907422

File tree

9 files changed

+446
-0
lines changed

9 files changed

+446
-0
lines changed

config/settings.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
'table' => null,
4141
'connection' => null,
4242
],
43+
'user_tenant' => [
44+
'type' => \Eclipse\Core\Settings\Repositories\UserSiteSettingsRepository::class,
45+
'model' => null,
46+
'table' => 'user_site_settings',
47+
'connection' => null,
48+
],
4349
'redis' => [
4450
'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class,
4551
'connection' => null,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::create('user_site_settings', function (Blueprint $table) {
15+
$table->id();
16+
17+
$table->foreignId('user_id')
18+
->nullable()
19+
->constrained('users')
20+
->cascadeOnDelete()
21+
->cascadeOnUpdate();
22+
23+
$table->foreignId('site_id')
24+
->nullable()
25+
->constrained('sites')
26+
->cascadeOnDelete()
27+
->cascadeOnUpdate();
28+
29+
$table->string('group');
30+
$table->string('name');
31+
$table->boolean('locked')->default(false);
32+
$table->json('payload');
33+
34+
$table->timestamps();
35+
36+
$table->unique(['group', 'name', 'user_id', 'site_id']);
37+
});
38+
}
39+
40+
/**
41+
* Reverse the migrations.
42+
*/
43+
public function down(): void
44+
{
45+
Schema::dropIfExists('user_site_settings');
46+
}
47+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
use Spatie\LaravelSettings\Migrations\SettingsMigration;
4+
5+
return new class extends SettingsMigration
6+
{
7+
public function up(): void
8+
{
9+
$this->migrator->repository('user_tenant');
10+
11+
$this->migrator->add('site.outgoing_email_address', '');
12+
$this->migrator->add('site.outgoing_email_signature', '');
13+
}
14+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Eclipse\Core\Filament\Pages;
4+
5+
use Eclipse\Core\Settings\UserSettings;
6+
use Filament\Forms\Components;
7+
use Filament\Forms\Form;
8+
use Filament\Pages\SettingsPage;
9+
10+
class ManageUserSettings extends SettingsPage
11+
{
12+
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
13+
14+
protected static string $settings = UserSettings::class;
15+
16+
public function form(Form $form): Form
17+
{
18+
return $form
19+
->schema([
20+
Components\Section::make('Email settings')
21+
->schema([
22+
Components\TextInput::make('outgoing_email_address')
23+
->email()
24+
->label('Outgoing email address'),
25+
Components\RichEditor::make('outgoing_email_signature')
26+
->label('Outgoing email signature'),
27+
]),
28+
]);
29+
}
30+
31+
public static function getNavigationGroup(): ?string
32+
{
33+
return 'Configuration';
34+
}
35+
36+
public static function getNavigationLabel(): string
37+
{
38+
return 'My settings';
39+
}
40+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace Eclipse\Core\Foundation\Settings;
4+
5+
use Eclipse\Core\Settings\Repositories\UserSiteSettingsRepository;
6+
use ReflectionClass;
7+
use ReflectionProperty;
8+
use RuntimeException;
9+
10+
trait IsUserSiteScoped
11+
{
12+
public static function repository(): ?string
13+
{
14+
return 'user_tenant';
15+
}
16+
17+
/**
18+
* Get settings for a specific user
19+
*
20+
* @param int $userId The ID of the user to get settings for
21+
* @return static
22+
*/
23+
public static function forUser(int $userId): static
24+
{
25+
// Create a new instance of UserSettings
26+
$settings = new static();
27+
28+
// Get the repository from the settings instance
29+
$repository = $settings->getRepository();
30+
31+
// Make sure it's a UserSettingsRepository
32+
if (!$repository instanceof UserSiteSettingsRepository) {
33+
throw new RuntimeException('Repository must be an instance of UserSiteSettingsRepository');
34+
}
35+
36+
// Configure the repository to use the specified user
37+
$userRepository = $repository->forUser($userId);
38+
39+
// Get the properties directly from the repository
40+
$properties = collect($userRepository->getPropertiesInGroup(static::group()));
41+
42+
// Process the properties (decrypt, cast, etc.)
43+
$reflectionProperties = collect((new ReflectionClass(static::class))->getProperties(ReflectionProperty::IS_PUBLIC))
44+
->mapWithKeys(fn (ReflectionProperty $property) => [$property->getName() => $property]);
45+
46+
// Set the properties on the settings instance
47+
foreach ($reflectionProperties as $name => $property) {
48+
if (isset($properties[$name])) {
49+
$settings->$name = $properties[$name];
50+
} elseif ($property->hasDefaultValue()) {
51+
$settings->$name = $property->getDefaultValue();
52+
}
53+
}
54+
55+
return $settings;
56+
}
57+
}

src/Models/User.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Eclipse\Core\Models;
44

55
use Eclipse\Core\Database\Factories\UserFactory;
6+
use Eclipse\Core\Settings\UserSettings;
67
use Exception;
78
use Filament\Models\Contracts\FilamentUser;
89
use Filament\Models\Contracts\HasAvatar;
@@ -14,6 +15,7 @@
1415
use Illuminate\Foundation\Auth\User as Authenticatable;
1516
use Illuminate\Notifications\Notifiable;
1617
use Illuminate\Support\Collection;
18+
use Spatie\LaravelSettings\Settings;
1719
use Spatie\MediaLibrary\HasMedia;
1820
use Spatie\MediaLibrary\InteractsWithMedia;
1921
use Spatie\Permission\Traits\HasRoles;
@@ -158,4 +160,9 @@ public function canImpersonate(): bool
158160
{
159161
return $this->can('impersonate', User::class);
160162
}
163+
164+
public function getSettings(string $settingsClass = UserSettings::class): Settings
165+
{
166+
return $settingsClass::forUser($this->id);
167+
}
161168
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Eclipse\Core\Settings\Repositories;
4+
5+
use Filament\Facades\Filament;
6+
use Illuminate\Database\Eloquent\Builder;
7+
use Illuminate\Database\Query\Builder as QueryBuilder;
8+
use Illuminate\Support\Facades\DB;
9+
use Spatie\LaravelSettings\Models\SettingsProperty;
10+
use Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository;
11+
12+
class UserSiteSettingsRepository extends DatabaseSettingsRepository
13+
{
14+
protected ?int $userId = null;
15+
16+
public function updatePropertiesPayload(string $group, array $properties): void
17+
{
18+
$propertiesInBatch = collect($properties)->map(function ($payload, $name) use ($group) {
19+
return [
20+
'group' => $group,
21+
'name' => $name,
22+
'payload' => $this->encode($payload),
23+
'site_id' => Filament::getTenant()?->id,
24+
'user_id' => auth()->user()?->id,
25+
];
26+
})->values()->toArray();
27+
28+
$this->getBuilder(false)
29+
->where('group', $group)
30+
->upsert($propertiesInBatch, ['group', 'name', 'site_id', 'user_id'], ['payload']);
31+
}
32+
33+
public function forUser(int $userId): self
34+
{
35+
$clone = clone $this;
36+
$clone->userId = $userId;
37+
return $clone;
38+
}
39+
40+
public function getBuilder(bool $fallback = true): Builder
41+
{
42+
$builder = parent::getBuilder();
43+
$userId = $this->userId ?? auth()->user()?->id;
44+
45+
if ($fallback) {
46+
// Use default fallback
47+
$table = $this->table ?? (new SettingsProperty)->getTable();
48+
$builder
49+
->where(function (Builder $query) use ($table, $userId) {
50+
$query
51+
->where(function (Builder $query) use ($table, $userId) {
52+
$query
53+
// ... where site_id matches
54+
->where('site_id', Filament::getTenant()?->id)
55+
// ... where user_id matches
56+
->where('user_id', $userId);
57+
})
58+
// ... or where site_id is null and a record with a matching site_id does not exist
59+
->orWhere(function (Builder $query) use ($table, $userId) {
60+
$query
61+
->whereNull('site_id')
62+
->whereNull('user_id')
63+
->whereNotExists(function (QueryBuilder $query) use ($table, $userId) {
64+
$query->select(DB::raw(1))
65+
->from($table, 't2')
66+
->where('site_id', Filament::getTenant()?->id)
67+
->where('user_id', $userId)
68+
->whereColumn('t2.group', $table.'.group')
69+
->whereColumn('t2.name', $table.'.name');
70+
});
71+
});
72+
});
73+
} else {
74+
// Don't use fallback, get only settings with the exact site/user match
75+
$builder
76+
->where('site_id', Filament::getTenant()?->id)
77+
->where('user_id', $userId);
78+
}
79+
80+
return $builder;
81+
}
82+
}

src/Settings/UserSettings.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Eclipse\Core\Settings;
4+
5+
use Eclipse\Core\Foundation\Settings\IsUserSiteScoped;
6+
use Spatie\LaravelSettings\Settings;
7+
8+
class UserSettings extends Settings
9+
{
10+
use IsUserSiteScoped;
11+
12+
public string $outgoing_email_address;
13+
14+
public string $outgoing_email_signature;
15+
16+
public static function group(): string
17+
{
18+
return 'site';
19+
}
20+
}

0 commit comments

Comments
 (0)