From 10787aada885bd389eec36db968dbd08c37650ca Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 28 Aug 2021 15:47:33 +0200 Subject: [PATCH] New user groups and memberships --- .../Upgrade/CreateGroupMemberships.php | 130 ++++++++++++++++++ app/Handlers/Events/UserEventHandler.php | 29 ++++ app/Http/Controllers/HomeController.php | 1 + app/Models/GroupMembership.php | 58 ++++++++ app/Models/UserGroup.php | 43 ++++++ app/Models/UserRole.php | 50 +++++++ app/Providers/EventServiceProvider.php | 1 + app/User.php | 28 ++-- config/user_roles.php | 36 +++++ .../2021_08_28_073733_user_groups.php | 74 ++++++++++ database/seeders/DatabaseSeeder.php | 1 + database/seeders/UserRoleSeeder.php | 40 ++++++ 12 files changed, 483 insertions(+), 8 deletions(-) create mode 100644 app/Console/Commands/Upgrade/CreateGroupMemberships.php create mode 100644 app/Models/GroupMembership.php create mode 100644 app/Models/UserGroup.php create mode 100644 app/Models/UserRole.php create mode 100644 config/user_roles.php create mode 100644 database/migrations/2021_08_28_073733_user_groups.php create mode 100644 database/seeders/UserRoleSeeder.php diff --git a/app/Console/Commands/Upgrade/CreateGroupMemberships.php b/app/Console/Commands/Upgrade/CreateGroupMemberships.php new file mode 100644 index 0000000000..03c8569f36 --- /dev/null +++ b/app/Console/Commands/Upgrade/CreateGroupMemberships.php @@ -0,0 +1,130 @@ +isExecuted() && true !== $this->option('force')) { + $this->warn('This command has already been executed.'); + + return 0; + } + $this->createGroupMemberships(); + $this->markAsExecuted(); + + $end = round(microtime(true) - $start, 2); + $this->info(sprintf('in %s seconds.', $end)); + + return 0; + } + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; + } + + /** + * + * @throws FireflyException + */ + private function createGroupMemberships(): void + { + $users = User::get(); + /** @var User $user */ + foreach ($users as $user) { + Log::debug(sprintf('Manage group memberships for user #%d', $user->id)); + if (!$this->hasGroupMembership($user)) { + Log::debug(sprintf('User #%d has no main group.', $user->id)); + $this->createGroupMembership($user); + } + Log::debug(sprintf('Done with user #%d', $user->id)); + } + } + + /** + * @param User $user + * + * @return bool + */ + private function hasGroupMembership(User $user): bool + { + return $user->groupMemberships()->count() > 0; + } + + /** + * @param User $user + * + * @throws FireflyException + */ + private function createGroupMembership(User $user): void + { + $userGroup = UserGroup::create(['title' => $user->email]); + $userRole = UserRole::where('title', UserRole::FULL)->first(); + + if (null === $userRole) { + throw new FireflyException('Firefly III could not find a user role. Please make sure all validations have run.'); + } + + $membership = GroupMembership::create( + [ + 'user_id' => $user->id, + 'user_role_id' => $userRole->id, + 'user_group_id' => $userGroup->id, + ] + ); + if (null === $membership) { + throw new FireflyException('Firefly III could not create user group management object. Please make sure all validations have run.'); + } + Log::debug(sprintf('User #%d now has main group.', $user->id)); + } + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } +} diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 2c5faa42a8..df5a6db661 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -35,6 +35,9 @@ use FireflyIII\Mail\NewIPAddressWarningMail; use FireflyIII\Mail\RegisteredUser as RegisteredUserMail; use FireflyIII\Mail\RequestedNewPassword as RequestedNewPasswordMail; use FireflyIII\Mail\UndoEmailChangeMail; +use FireflyIII\Models\GroupMembership; +use FireflyIII\Models\UserGroup; +use FireflyIII\Models\UserRole; use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Auth\Events\Login; @@ -248,6 +251,32 @@ class UserEventHandler return true; } + /** + * @param RegisteredUser $event + * + * @return bool + * @throws FireflyException + */ + public function createGroupMembership(RegisteredUser $event): bool + { + $user = $event->user; + // create a new group. + $group = UserGroup::create(['title' => $user->email]); + $role = UserRole::where('title', UserRole::FULL)->first(); + if (null === $role) { + throw new FireflyException('The user role is unexpectedly empty. Did you run all migrations?'); + } + GroupMembership::create( + [ + 'user_id' => $user->id, + 'user_group_id' => $group->id, + 'user_role_id' => $role->id, + ] + ); + + return true; + } + /** * This method will send the user a registration mail, welcoming him or her to Firefly III. * This message is only sent when the configuration of Firefly III says so. diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index cff4d19f0c..cc5f6c3bdf 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -28,6 +28,7 @@ use FireflyIII\Events\RequestedVersionCheckStatus; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Http\Middleware\Installer; use FireflyIII\Models\AccountType; +use FireflyIII\Models\GroupMembership; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\User; diff --git a/app/Models/GroupMembership.php b/app/Models/GroupMembership.php new file mode 100644 index 0000000000..e1bed551cf --- /dev/null +++ b/app/Models/GroupMembership.php @@ -0,0 +1,58 @@ +. + */ + +namespace FireflyIII\Models; + +use FireflyIII\User; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +/** + * Class GroupMembership + */ +class GroupMembership extends Model +{ + protected $fillable = ['user_id', 'user_group_id', 'user_role_id']; + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * @return BelongsTo + */ + public function userGroup(): BelongsTo + { + return $this->belongsTo(UserGroup::class); + } + + /** + * @return BelongsTo + */ + public function userRole(): BelongsTo + { + return $this->belongsTo(UserRole::class); + } +} \ No newline at end of file diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php new file mode 100644 index 0000000000..043e0fce8e --- /dev/null +++ b/app/Models/UserGroup.php @@ -0,0 +1,43 @@ +. + */ + +namespace FireflyIII\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; + +/** + * Class UserGroup + */ +class UserGroup extends Model +{ + protected $fillable = ['title']; + + /** + * @codeCoverageIgnore + * + * @return HasMany + */ + public function groupMemberships(): HasMany + { + return $this->hasMany(GroupMembership::class); + } +} \ No newline at end of file diff --git a/app/Models/UserRole.php b/app/Models/UserRole.php new file mode 100644 index 0000000000..1ddf1252f8 --- /dev/null +++ b/app/Models/UserRole.php @@ -0,0 +1,50 @@ +. + */ + +namespace FireflyIII\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; + +/** + * Class UserRole + */ +class UserRole extends Model +{ + public const READ_ONLY = 'ro'; + public const CHANGE_TRANSACTIONS = 'change_tx'; + public const CHANGE_RULES = 'change_rules'; + public const CHANGE_PIGGY_BANKS = 'change_piggies'; + public const CHANGE_REPETITIONS = 'change_reps'; + public const VIEW_REPORTS = 'view_reports'; + public const FULL = 'full'; + protected $fillable = ['title']; + + /** + * @codeCoverageIgnore + * + * @return HasMany + */ + public function groupMemberships(): HasMany + { + return $this->hasMany(GroupMembership::class); + } +} \ No newline at end of file diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 02139dd890..c9ff95a863 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -68,6 +68,7 @@ class EventServiceProvider extends ServiceProvider RegisteredUser::class => [ 'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail', 'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole', + 'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership', ], // is a User related event. Login::class => [ diff --git a/app/User.php b/app/User.php index f16cd5a0f3..f6eaf697dd 100644 --- a/app/User.php +++ b/app/User.php @@ -34,6 +34,7 @@ use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; use FireflyIII\Models\CurrencyExchangeRate; +use FireflyIII\Models\GroupMembership; use FireflyIII\Models\ObjectGroup; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\Preference; @@ -136,14 +137,14 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @method static Builder|User whereObjectguid($value) * @property string|null $provider * @method static Builder|User whereProvider($value) - * @property-read \Illuminate\Database\Eloquent\Collection|ObjectGroup[] $objectGroups - * @property-read int|null $object_groups_count - * @property-read \Illuminate\Database\Eloquent\Collection|Webhook[] $webhooks - * @property-read int|null $webhooks_count - * @property string|null $two_factor_secret - * @property string|null $two_factor_recovery_codes - * @property string|null $guid - * @property string|null $domain + * @property-read \Illuminate\Database\Eloquent\Collection|ObjectGroup[] $objectGroups + * @property-read int|null $object_groups_count + * @property-read \Illuminate\Database\Eloquent\Collection|Webhook[] $webhooks + * @property-read int|null $webhooks_count + * @property string|null $two_factor_secret + * @property string|null $two_factor_recovery_codes + * @property string|null $guid + * @property string|null $domain * @method static Builder|User whereDomain($value) * @method static Builder|User whereGuid($value) * @method static Builder|User whereTwoFactorRecoveryCodes($value) @@ -212,6 +213,16 @@ class User extends Authenticatable return $this->hasMany(Account::class); } + /** + * @codeCoverageIgnore + * + * @return HasMany + */ + public function groupMemberships(): HasMany + { + return $this->hasMany(GroupMembership::class)->with(['userGroup','userRole']); + } + /** * @codeCoverageIgnore * Link to attachments @@ -449,6 +460,7 @@ class User extends Authenticatable } // start LDAP related code + /** * Get the database column name of the domain. * diff --git a/config/user_roles.php b/config/user_roles.php new file mode 100644 index 0000000000..033d0a8693 --- /dev/null +++ b/config/user_roles.php @@ -0,0 +1,36 @@ +. + */ + + +use FireflyIII\Models\UserRole; + +return [ + + 'roles' => [ + UserRole::READ_ONLY => [], + UserRole::CHANGE_TRANSACTIONS => [], + UserRole::CHANGE_RULES => [], + UserRole::CHANGE_PIGGY_BANKS => [], + UserRole::CHANGE_REPETITIONS => [], + UserRole::VIEW_REPORTS => [], + UserRole::FULL => [], + ], +]; \ No newline at end of file diff --git a/database/migrations/2021_08_28_073733_user_groups.php b/database/migrations/2021_08_28_073733_user_groups.php new file mode 100644 index 0000000000..c981a64e14 --- /dev/null +++ b/database/migrations/2021_08_28_073733_user_groups.php @@ -0,0 +1,74 @@ +bigIncrements('id'); + $table->timestamps(); + $table->softDeletes(); + + $table->string('title', 255); + $table->unique('title'); + } + ); + + Schema::create( + 'user_roles', static function (Blueprint $table) { + $table->bigIncrements('id'); + $table->timestamps(); + $table->softDeletes(); + + $table->string('title', 255); + $table->unique('title'); + } + ); + + Schema::create( + 'group_memberships', + static function (Blueprint $table) { + $table->bigIncrements('id'); + $table->timestamps(); + $table->softDeletes(); + $table->integer('user_id', false, true); + $table->bigInteger('user_group_id', false, true); + $table->bigInteger('user_role_id', false, true); + + $table->foreign('user_id')->references('id')->on('users')->onUpdate('cascade')->onDelete('cascade'); + $table->foreign('user_group_id')->references('id')->on('user_groups')->onUpdate('cascade')->onDelete('cascade'); + $table->foreign('user_role_id')->references('id')->on('user_roles')->onUpdate('cascade')->onDelete('cascade'); + $table->unique(['user_id', 'user_group_id', 'user_role_id']); + } + ); + + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 128795bbdc..839742aa3c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -40,5 +40,6 @@ class DatabaseSeeder extends Seeder $this->call(PermissionSeeder::class); $this->call(LinkTypeSeeder::class); $this->call(ConfigSeeder::class); + $this->call(UserRoleSeeder::class); } } diff --git a/database/seeders/UserRoleSeeder.php b/database/seeders/UserRoleSeeder.php new file mode 100644 index 0000000000..48d7e73cdf --- /dev/null +++ b/database/seeders/UserRoleSeeder.php @@ -0,0 +1,40 @@ + $role]); + } catch (PDOException $e) { + // @ignoreException + } + } + } +}