Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
'table' => null,
'connection' => null,
],
'user_tenant' => [
'type' => \Eclipse\Core\Settings\Repositories\UserSiteSettingsRepository::class,
'model' => null,
'table' => 'user_site_settings',
'connection' => null,
],
'redis' => [
'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class,
'connection' => null,
Expand Down
47 changes: 47 additions & 0 deletions database/migrations/2025_06_23_110245_create_user_settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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::create('user_site_settings', function (Blueprint $table) {
$table->id();

$table->foreignId('user_id')
->nullable()
->constrained('users')
->cascadeOnDelete()
->cascadeOnUpdate();

$table->foreignId('site_id')
->nullable()
->constrained('sites')
->cascadeOnDelete()
->cascadeOnUpdate();

$table->string('group');
$table->string('name');
$table->boolean('locked')->default(false);
$table->json('payload');

$table->timestamps();

$table->unique(['group', 'name', 'user_id', 'site_id']);
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_site_settings');
}
};
14 changes: 14 additions & 0 deletions database/settings/2025_06_23_115758_create_user_settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

use Spatie\LaravelSettings\Migrations\SettingsMigration;

return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->repository('user_tenant');

$this->migrator->add('site.outgoing_email_address', '');
$this->migrator->add('site.outgoing_email_signature', '');
}
};
40 changes: 40 additions & 0 deletions src/Filament/Pages/ManageUserSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Eclipse\Core\Filament\Pages;

use Eclipse\Core\Settings\UserSettings;
use Filament\Forms\Components;
use Filament\Forms\Form;
use Filament\Pages\SettingsPage;

class ManageUserSettings extends SettingsPage
{
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';

protected static string $settings = UserSettings::class;

public function form(Form $form): Form
{
return $form
->schema([
Components\Section::make('Email settings')
->schema([
Components\TextInput::make('outgoing_email_address')
->email()
->label('Outgoing email address'),
Components\RichEditor::make('outgoing_email_signature')
->label('Outgoing email signature'),
]),
]);
}

public static function getNavigationGroup(): ?string
{
return 'Configuration';
}

public static function getNavigationLabel(): string
{
return 'My settings';
}
}
56 changes: 56 additions & 0 deletions src/Foundation/Settings/IsUserSiteScoped.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Eclipse\Core\Foundation\Settings;

use Eclipse\Core\Settings\Repositories\UserSiteSettingsRepository;
use ReflectionClass;
use ReflectionProperty;
use RuntimeException;

trait IsUserSiteScoped
{
public static function repository(): ?string
{
return 'user_tenant';
}

/**
* Get settings for a specific user
*
* @param int $userId The ID of the user to get settings for
*/
public static function forUser(int $userId): static
{
// Create a new instance of UserSettings
$settings = new static;

// Get the repository from the settings instance
$repository = $settings->getRepository();

// Make sure it's a UserSettingsRepository
if (! $repository instanceof UserSiteSettingsRepository) {
throw new RuntimeException('Repository must be an instance of UserSiteSettingsRepository');
}

// Configure the repository to use the specified user
$userRepository = $repository->forUser($userId);

// Get the properties directly from the repository
$properties = collect($userRepository->getPropertiesInGroup(static::group()));

// Process the properties (decrypt, cast, etc.)
$reflectionProperties = collect((new ReflectionClass(static::class))->getProperties(ReflectionProperty::IS_PUBLIC))
->mapWithKeys(fn (ReflectionProperty $property) => [$property->getName() => $property]);

// Set the properties on the settings instance
foreach ($reflectionProperties as $name => $property) {
if (isset($properties[$name])) {
$settings->$name = $properties[$name];
} elseif ($property->hasDefaultValue()) {
$settings->$name = $property->getDefaultValue();
}
}

return $settings;
}
}
7 changes: 7 additions & 0 deletions src/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Eclipse\Core\Models;

use Eclipse\Core\Database\Factories\UserFactory;
use Eclipse\Core\Settings\UserSettings;
use Exception;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
Expand All @@ -14,6 +15,7 @@
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Spatie\LaravelSettings\Settings;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\Permission\Traits\HasRoles;
Expand Down Expand Up @@ -158,4 +160,9 @@ public function canImpersonate(): bool
{
return $this->can('impersonate', User::class);
}

public function getSettings(string $settingsClass = UserSettings::class): Settings
{
return $settingsClass::forUser($this->id);
}
}
83 changes: 83 additions & 0 deletions src/Settings/Repositories/UserSiteSettingsRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Eclipse\Core\Settings\Repositories;

use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Facades\DB;
use Spatie\LaravelSettings\Models\SettingsProperty;
use Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository;

class UserSiteSettingsRepository extends DatabaseSettingsRepository
{
protected ?int $userId = null;

public function updatePropertiesPayload(string $group, array $properties): void
{
$propertiesInBatch = collect($properties)->map(function ($payload, $name) use ($group) {
return [
'group' => $group,
'name' => $name,
'payload' => $this->encode($payload),
'site_id' => Filament::getTenant()?->id,
'user_id' => auth()->user()?->id,
];
})->values()->toArray();

$this->getBuilder(false)
->where('group', $group)
->upsert($propertiesInBatch, ['group', 'name', 'site_id', 'user_id'], ['payload']);
}

public function forUser(int $userId): self
{
$clone = clone $this;
$clone->userId = $userId;

return $clone;
}

public function getBuilder(bool $fallback = true): Builder
{
$builder = parent::getBuilder();
$userId = $this->userId ?? auth()->user()?->id;

if ($fallback) {
// Use default fallback
$table = $this->table ?? (new SettingsProperty)->getTable();
$builder
->where(function (Builder $query) use ($table, $userId) {
$query
->where(function (Builder $query) use ($userId) {
$query
// ... where site_id matches
->where('site_id', Filament::getTenant()?->id)
// ... where user_id matches
->where('user_id', $userId);
})
// ... or where site_id is null and a record with a matching site_id does not exist
->orWhere(function (Builder $query) use ($table, $userId) {
$query
->whereNull('site_id')
->whereNull('user_id')
->whereNotExists(function (QueryBuilder $query) use ($table, $userId) {
$query->select(DB::raw(1))
->from($table, 't2')
->where('site_id', Filament::getTenant()?->id)
->where('user_id', $userId)
->whereColumn('t2.group', $table.'.group')
->whereColumn('t2.name', $table.'.name');
});
});
});
} else {
// Don't use fallback, get only settings with the exact site/user match
$builder
->where('site_id', Filament::getTenant()?->id)
->where('user_id', $userId);
}

return $builder;
}
}
20 changes: 20 additions & 0 deletions src/Settings/UserSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Eclipse\Core\Settings;

use Eclipse\Core\Foundation\Settings\IsUserSiteScoped;
use Spatie\LaravelSettings\Settings;

class UserSettings extends Settings
{
use IsUserSiteScoped;

public string $outgoing_email_address;

public string $outgoing_email_signature;

public static function group(): string
{
return 'site';
}
}
Loading