diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index f6bcebe..2dde75f 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -29,3 +29,4 @@ jobs: - uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: "Prettify code" + commit_options: '--no-verify' diff --git a/README.md b/README.md index 03fd77b..0aec326 100644 --- a/README.md +++ b/README.md @@ -87,3 +87,15 @@ Or in Blade: ``` Don't forget to add the feature flags to your navigation too. + +## Allow users logged in Twill option +You can allow your users to have access to a feature in public available domains if they are logged in on Twill. But there are some caveats for it to work: + +- Twill must be int he same domain of the web application, meaning that `ADMIN_APP_URL` must be empty or have the same domain as the frontend; or +- The Laravel session domain should set in order for the apps to share the session cookie: + +```dotenv +SESSION_DOMAIN=.laravel-twill-project.test +``` + +Also, make sure your sessions are working fine, when you switch to a shared domain they might break and a browser cookie clear might be needed. diff --git a/docs/screenshot02.png b/docs/screenshot02.png index c8de88d..80302e8 100644 Binary files a/docs/screenshot02.png and b/docs/screenshot02.png differ diff --git a/phpstan.neon b/phpstan.neon index 964a330..0392d53 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,12 +9,10 @@ parameters: excludePaths: - checkMissingIterableValueType: false - universalObjectCratesClasses: ignoreErrors: + - identifier: missingType.generics + - identifier: missingType.iterableValue reportUnmatchedIgnoredErrors: false - - checkGenericClassInNonGenericObjectType: false diff --git a/src/Http/Controllers/TwillFeatureFlagController.php b/src/Http/Controllers/TwillFeatureFlagController.php index 0a7fbf0..7c906b5 100644 --- a/src/Http/Controllers/TwillFeatureFlagController.php +++ b/src/Http/Controllers/TwillFeatureFlagController.php @@ -27,6 +27,12 @@ class TwillFeatureFlagController extends ModuleController 'sort' => true, ], + 'publicly_available_twill_users' => [ + 'title' => 'Publicly available for Twill users', + 'field' => 'publicly_available_twill_users_yes_no', + 'sort' => true, + ], + 'publicly_available_ips' => [ 'title' => 'Publicly available to (IPs)', 'field' => 'publicly_available_ips', diff --git a/src/Models/TwillFeatureFlag.php b/src/Models/TwillFeatureFlag.php index 1ef27a2..fc5b233 100644 --- a/src/Models/TwillFeatureFlag.php +++ b/src/Models/TwillFeatureFlag.php @@ -4,7 +4,9 @@ use A17\Twill\Models\Model; use Illuminate\Support\Str; +use A17\Twill\Models\Behaviors\HasRelated; use A17\Twill\Models\Behaviors\HasRevisions; +use Illuminate\Foundation\Auth\User as AuthenticatableContract; /** * @property string $code @@ -14,12 +16,22 @@ * @property string $publicly_available_yes_no * @property string|null $publicly_available_ips * @property bool $published + * @property bool $publicly_available_twill_users */ class TwillFeatureFlag extends Model { + use HasRelated; use HasRevisions; - protected $fillable = ['published', 'title', 'description', 'code', 'publicly_available', 'ip_addresses']; + protected $fillable = [ + 'published', + 'title', + 'description', + 'code', + 'publicly_available', + 'ip_addresses', + 'publicly_available_twill_users', + ]; /** * Save the model to the database. @@ -39,8 +51,39 @@ public function getPubliclyAvailableYesNoAttribute(): string return $this->publicly_available ? 'Yes' : ''; } + public function getPubliclyAvailableTwillUsersYesNoAttribute(): string + { + return $this->publicly_available_twill_users ? 'Yes' : ''; + } + public function getPubliclyAvailableIpsAttribute(): string|null { return $this->ip_addresses ?? null; } + + public function userIsPubliclyAllowed(AuthenticatableContract|null $user): bool + { + if ($user === null) { + return false; + } + + /** @phpstan-ignore-next-line */ + if ($user->published === false) { + return false; + } + + /** @phpstan-ignore-next-line */ + if ($user->isSuperAdmin()) { + return true; + } + + $allowedUsers = $this->getRelated('allowed_twill_users'); + + if ($allowedUsers->isEmpty()) { + return true; + } + + /** @phpstan-ignore-next-line */ + return $allowedUsers->pluck('email')->contains($user->email); + } } diff --git a/src/Repositories/TwillFeatureFlagRepository.php b/src/Repositories/TwillFeatureFlagRepository.php index a5c2e18..6fa2c80 100644 --- a/src/Repositories/TwillFeatureFlagRepository.php +++ b/src/Repositories/TwillFeatureFlagRepository.php @@ -14,6 +14,10 @@ class TwillFeatureFlagRepository extends ModuleRepository { use HandleRevisions; + protected $relatedBrowsers = [ + 'allowed_twill_users' => ['moduleName' => 'users', 'relation' => 'allowed_twill_users'], + ]; + public function __construct(TwillFeatureFlag $model = null) { $this->bootCache(); @@ -40,7 +44,7 @@ public function getFeature(string $code): bool return false; } - if (blank($featureFlag) || blank($featureFlag?->published) || $featureFlag?->published === false) { + if (blank($featureFlag) || blank($featureFlag->published) || $featureFlag->published === false) { return false; } @@ -53,7 +57,8 @@ public function getFeature(string $code): bool private function isRealProduction(): bool { - return (new Collection(config('app.domains.publicly_available')))->contains(request()->getHost()); + return app()->environment('production') && + (new Collection(config('app.domains.publicly_available')))->contains(request()->getHost()); } public function featureList(bool $all = false): array @@ -73,11 +78,8 @@ public function featureList(bool $all = false): array private function isPubliclyAvailableToCurrentUser(TwillFeatureFlag $featureFlag): bool { - return (new GeolocationService())->currentIpAddressIsOnList( - collect(explode(',', $featureFlag->ip_addresses)) - ->map(fn($ip) => trim($ip)) - ->toArray(), - ); + return $this->isPubliclyAvailableToIpAddresses($featureFlag) || + $this->isPubliclyAvailableToTwillUsers($featureFlag); } private function bootCache(): void @@ -132,4 +134,28 @@ protected function url(array|bool $parsed, string $attribute): string|null return $parsed[$attribute]; } + + public function isPubliclyAvailableToIpAddresses(TwillFeatureFlag $featureFlag): bool + { + if (blank($featureFlag->ip_addresses)) { + return false; + } + + return (new GeolocationService())->currentIpAddressIsOnList( + collect(explode(',', $featureFlag->ip_addresses)) + ->map(fn($ip) => trim($ip)) + ->toArray(), + ); + } + + public function isPubliclyAvailableToTwillUsers(TwillFeatureFlag $featureFlag): bool + { + if (!$featureFlag->publicly_available_twill_users) { + return false; + } + + $auth = auth('twill_users'); + + return $auth->check() && $featureFlag->userIsPubliclyAllowed($auth->user()); + } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 672b137..1b4e421 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -5,6 +5,7 @@ use Illuminate\Support\Str; use A17\Twill\Facades\TwillCapsules; use A17\Twill\TwillPackageServiceProvider; +use A17\TwillFeatureFlags\Services\Helpers; class ServiceProvider extends TwillPackageServiceProvider { @@ -15,6 +16,8 @@ public function boot(): void { $this->registerThisCapsule(); + Helpers::load(); + parent::boot(); } diff --git a/src/Services/Helpers.php b/src/Services/Helpers.php index 82524e1..2ece564 100644 --- a/src/Services/Helpers.php +++ b/src/Services/Helpers.php @@ -6,6 +6,6 @@ class Helpers { public static function load(): void { - require __DIR__ . '/../Support/helpers.php'; + require_once __DIR__ . '/../Support/helpers.php'; } } diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 4825e5a..41ed863 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -15,3 +15,27 @@ function feature_list(bool $all = false): array return (new TwillFeatureFlagRepository())->featureList($all); } } + +if (!function_exists('features_can_be_public_on_twill')) { + function features_can_be_public_on_twill(): bool + { + $sessionDomain = config('session.domain'); + + $twillAdminAppHost = parse_url(config('twill.admin_app_url') ?? '')['host'] ?? null; + + $appDomainHost = parse_url(config('app.url') ?? '')['host'] ?? null; + + // If the admin app is not set it probably means Twill is in the same domain of the frontend + if (blank($twillAdminAppHost)) { + return true; + } + + // Otherwise the domain must be the same + if ($twillAdminAppHost === $appDomainHost) { + return true; + } + + // Otherwise the session domain must be set + return filled($sessionDomain); + } +} diff --git a/src/database/migrations/2024_06_04_114937_add_public_available_for_twill.php b/src/database/migrations/2024_06_04_114937_add_public_available_for_twill.php new file mode 100644 index 0000000..4a07a6b --- /dev/null +++ b/src/database/migrations/2024_06_04_114937_add_public_available_for_twill.php @@ -0,0 +1,27 @@ +boolean('publicly_available_twill_users')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('twill_feature_flags', function (Blueprint $table) { + $table->dropColumn('publicly_available_twill_users'); + }); + } +}; diff --git a/src/resources/views/admin/form.blade.php b/src/resources/views/admin/form.blade.php index 0578ab9..ea8776d 100644 --- a/src/resources/views/admin/form.blade.php +++ b/src/resources/views/admin/form.blade.php @@ -7,6 +7,13 @@ 'type' => 'text' ]) + @formField('input', [ + 'label' => 'Description', + 'name' => 'description', + 'rows' => 4, + 'type' => 'textarea' + ]) + @formField('checkbox', [ 'name' => 'publicly_available', 'label' => 'Publicly available', @@ -22,10 +29,23 @@ 'translated' => false, ]) - @formField('input', [ - 'label' => 'Description', - 'name' => 'description', - 'rows' => 4, - 'type' => 'textarea' - ]) + @if (features_can_be_public_on_twill()) + @formField('checkbox', [ + 'name' => 'publicly_available_twill_users', + 'label' => 'Publicly available for users logged in Twill', + ]) + + @formConnectedFields([ + 'fieldName' => 'publicly_available_twill_users', + 'fieldValues' => true, + ]) + @formField('browser', [ + 'moduleName' => 'users', + 'name' => 'allowed_twill_users', + 'label' => 'Allowed users', + 'note' => 'If no users are selected, all users will be allowed', + 'max' => 999, + ]) + @endformConnectedFields + @endif @stop