From 724a16944a82feef9e5066a0f95c8875eebe3f36 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 21 Sep 2023 11:29:09 +0200 Subject: [PATCH] Better user group management and object observers --- .../UserGroup/UpdateController.php | 61 +++++- app/Api/V2/Request/UserGroup/StoreRequest.php | 2 + .../UserGroup/UpdateMembershipRequest.php | 69 ++++++ .../V2/Request/UserGroup/UpdateRequest.php | 64 ++++++ .../PiggyBank/ChangedAmount.php} | 7 +- .../{ => Model}/PiggyBankEventHandler.php | 8 +- app/Handlers/Observer/AccountObserver.php | 58 +++++ app/Handlers/Observer/AttachmentObserver.php | 36 +++ app/Handlers/Observer/BillObserver.php | 45 ++++ app/Handlers/Observer/BudgetObserver.php | 49 +++++ app/Handlers/Observer/CategoryObserver.php | 45 ++++ app/Handlers/Observer/PiggyBankObserver.php | 71 ++++++ app/Handlers/Observer/RecurrenceObserver.php | 52 +++++ .../RecurrenceTransactionObserver.php | 42 ++++ app/Handlers/Observer/RuleGroupObserver.php | 45 ++++ app/Handlers/Observer/RuleObserver.php | 43 ++++ app/Handlers/Observer/TagObserver.php | 48 ++++ .../Observer/TransactionGroupObserver.php | 39 ++++ .../Observer/TransactionJournalObserver.php | 55 +++++ app/Handlers/Observer/TransactionObserver.php | 37 ++++ .../Observer/WebhookMessageObserver.php | 43 ++++ app/Handlers/Observer/WebhookObserver.php | 44 ++++ app/Models/Attachment.php | 2 +- app/Models/Location.php | 4 +- app/Models/TransactionJournal.php | 10 +- app/Providers/EventServiceProvider.php | 74 +++++-- .../PiggyBank/ModifiesPiggyBanks.php | 13 +- .../UserGroup/UserGroupRepository.php | 205 +++++++++++++++--- .../UserGroupRepositoryInterface.php | 23 ++ app/Support/Request/ChecksLogin.php | 30 ++- routes/api.php | 2 + 31 files changed, 1253 insertions(+), 73 deletions(-) create mode 100644 app/Api/V2/Request/UserGroup/UpdateMembershipRequest.php create mode 100644 app/Api/V2/Request/UserGroup/UpdateRequest.php rename app/Events/{ChangedPiggyBankAmount.php => Model/PiggyBank/ChangedAmount.php} (93%) rename app/Handlers/Events/{ => Model}/PiggyBankEventHandler.php (91%) create mode 100644 app/Handlers/Observer/AccountObserver.php create mode 100644 app/Handlers/Observer/AttachmentObserver.php create mode 100644 app/Handlers/Observer/BillObserver.php create mode 100644 app/Handlers/Observer/BudgetObserver.php create mode 100644 app/Handlers/Observer/CategoryObserver.php create mode 100644 app/Handlers/Observer/PiggyBankObserver.php create mode 100644 app/Handlers/Observer/RecurrenceObserver.php create mode 100644 app/Handlers/Observer/RecurrenceTransactionObserver.php create mode 100644 app/Handlers/Observer/RuleGroupObserver.php create mode 100644 app/Handlers/Observer/RuleObserver.php create mode 100644 app/Handlers/Observer/TagObserver.php create mode 100644 app/Handlers/Observer/TransactionGroupObserver.php create mode 100644 app/Handlers/Observer/TransactionJournalObserver.php create mode 100644 app/Handlers/Observer/TransactionObserver.php create mode 100644 app/Handlers/Observer/WebhookMessageObserver.php create mode 100644 app/Handlers/Observer/WebhookObserver.php diff --git a/app/Api/V2/Controllers/UserGroup/UpdateController.php b/app/Api/V2/Controllers/UserGroup/UpdateController.php index 2daa1f84d5..8630952bf6 100644 --- a/app/Api/V2/Controllers/UserGroup/UpdateController.php +++ b/app/Api/V2/Controllers/UserGroup/UpdateController.php @@ -26,12 +26,71 @@ declare(strict_types=1); namespace FireflyIII\Api\V2\Controllers\UserGroup; use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Api\V2\Request\UserGroup\UpdateMembershipRequest; +use FireflyIII\Api\V2\Request\UserGroup\UpdateRequest; +use FireflyIII\Models\UserGroup; +use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface; +use FireflyIII\Transformers\V2\UserGroupTransformer; +use Illuminate\Http\JsonResponse; /** * Class UpdateController */ class UpdateController extends Controller { - // basic edit van group + // basic update van group // add user, add rights, remove user, remove rights. + + private UserGroupRepositoryInterface $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->repository = app(UserGroupRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @param UpdateRequest $request + * @param UserGroup $userGroup + * + * @return JsonResponse + */ + public function update(UpdateRequest $request, UserGroup $userGroup): JsonResponse + { + $all = $request->getAll(); + $userGroup = $this->repository->update($userGroup, $all); + $transformer = new UserGroupTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject('user-groups', $userGroup, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); + } + + /** + * @param UpdateMembershipRequest $request + * @param UserGroup $userGroup + * + * @return JsonResponse + */ + public function updateMembership(UpdateMembershipRequest $request, UserGroup $userGroup): JsonResponse + { + $all = $request->getAll(); + $userGroup = $this->repository->updateMembership($userGroup, $all); + $transformer = new UserGroupTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject('user-groups', $userGroup, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); + } } diff --git a/app/Api/V2/Request/UserGroup/StoreRequest.php b/app/Api/V2/Request/UserGroup/StoreRequest.php index 494417cbf5..54d6d54e8a 100644 --- a/app/Api/V2/Request/UserGroup/StoreRequest.php +++ b/app/Api/V2/Request/UserGroup/StoreRequest.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V2\Request\UserGroup; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; @@ -34,6 +35,7 @@ use Illuminate\Foundation\Http\FormRequest; */ class StoreRequest extends FormRequest { + protected array $acceptedRoles = [UserRoleEnum::OWNER, UserRoleEnum::FULL]; use ChecksLogin; use ConvertsDataTypes; diff --git a/app/Api/V2/Request/UserGroup/UpdateMembershipRequest.php b/app/Api/V2/Request/UserGroup/UpdateMembershipRequest.php new file mode 100644 index 0000000000..fb2e7a1edc --- /dev/null +++ b/app/Api/V2/Request/UserGroup/UpdateMembershipRequest.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V2\Request\UserGroup; + +use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Support\Request\ChecksLogin; +use FireflyIII\Support\Request\ConvertsDataTypes; +use Illuminate\Foundation\Http\FormRequest; + +/** + * Class StoreRequest + */ +class UpdateMembershipRequest extends FormRequest +{ + protected array $acceptedRoles = [UserRoleEnum::OWNER, UserRoleEnum::FULL]; + use ChecksLogin; + use ConvertsDataTypes; + + /** + * @return array + */ + public function getAll(): array + { + return [ + 'id' => $this->convertInteger('id'), + 'email' => $this->convertString('email'), + 'roles' => $this->get('roles') ?? [], + ]; + } + + /** + * @return array + */ + public function rules(): array + { + $validRoles = []; + foreach (UserRoleEnum::cases() as $role) { + $validRoles[] = $role->value; + } + return [ + 'id' => 'exists:users,id|required_without:email', + 'email' => 'exists:users,email|required_without:id', + 'roles.*' => 'required|in:' . join(',', $validRoles), + ]; + } +} diff --git a/app/Api/V2/Request/UserGroup/UpdateRequest.php b/app/Api/V2/Request/UserGroup/UpdateRequest.php new file mode 100644 index 0000000000..9acf9e0628 --- /dev/null +++ b/app/Api/V2/Request/UserGroup/UpdateRequest.php @@ -0,0 +1,64 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V2\Request\UserGroup; + +use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Models\UserGroup; +use FireflyIII\Support\Request\ChecksLogin; +use FireflyIII\Support\Request\ConvertsDataTypes; +use Illuminate\Foundation\Http\FormRequest; + +/** + * Class StoreRequest + */ +class UpdateRequest extends FormRequest +{ + protected array $acceptedRoles = [UserRoleEnum::OWNER, UserRoleEnum::FULL]; + use ChecksLogin; + use ConvertsDataTypes; + + /** + * @return array + */ + public function getAll(): array + { + return [ + 'title' => $this->convertString('title'), + ]; + } + + /** + * @return array + */ + public function rules(): array + { + /** @var UserGroup $userGroup */ + $userGroup = $this->route()->parameter('userGroup'); + return [ + 'title' => sprintf('required|min:2|max:255|unique:user_groups,title,%d', $userGroup->id), + ]; + } +} diff --git a/app/Events/ChangedPiggyBankAmount.php b/app/Events/Model/PiggyBank/ChangedAmount.php similarity index 93% rename from app/Events/ChangedPiggyBankAmount.php rename to app/Events/Model/PiggyBank/ChangedAmount.php index c1f6a7b689..79d2068c30 100644 --- a/app/Events/ChangedPiggyBankAmount.php +++ b/app/Events/Model/PiggyBank/ChangedAmount.php @@ -22,8 +22,9 @@ declare(strict_types=1); -namespace FireflyIII\Events; +namespace FireflyIII\Events\Model\PiggyBank; +use FireflyIII\Events\Event; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; @@ -31,9 +32,9 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; /** - * Class ChangedPiggyBankAmount + * Class ChangedAmount */ -class ChangedPiggyBankAmount extends Event +class ChangedAmount extends Event { use SerializesModels; diff --git a/app/Handlers/Events/PiggyBankEventHandler.php b/app/Handlers/Events/Model/PiggyBankEventHandler.php similarity index 91% rename from app/Handlers/Events/PiggyBankEventHandler.php rename to app/Handlers/Events/Model/PiggyBankEventHandler.php index 6d19541016..5eca00f652 100644 --- a/app/Handlers/Events/PiggyBankEventHandler.php +++ b/app/Handlers/Events/Model/PiggyBankEventHandler.php @@ -22,9 +22,9 @@ declare(strict_types=1); -namespace FireflyIII\Handlers\Events; +namespace FireflyIII\Handlers\Events\Model; -use FireflyIII\Events\ChangedPiggyBankAmount; +use FireflyIII\Events\Model\PiggyBank\ChangedAmount; use FireflyIII\Models\PiggyBankEvent; use Illuminate\Support\Facades\Log; @@ -34,11 +34,11 @@ use Illuminate\Support\Facades\Log; class PiggyBankEventHandler { /** - * @param ChangedPiggyBankAmount $event + * @param ChangedAmount $event * * @return void */ - public function changePiggyAmount(ChangedPiggyBankAmount $event): void + public function changePiggyAmount(ChangedAmount $event): void { // find journal if group is present. $journal = $event->transactionJournal; diff --git a/app/Handlers/Observer/AccountObserver.php b/app/Handlers/Observer/AccountObserver.php new file mode 100644 index 0000000000..b5ad9971a7 --- /dev/null +++ b/app/Handlers/Observer/AccountObserver.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Account; + +/** + * Class AccountObserver + */ +class AccountObserver +{ + /** + * Also delete related objects. + * + * @param Account $account + * + * @return void + */ + public function deleting(Account $account): void + { + app('log')->debug('Observe "deleting" of an account.'); + $account->accountMeta()->delete(); + foreach ($account->piggyBanks()->get() as $piggy) { + $piggy->delete(); + } + foreach ($account->attachments()->get() as $attachment) { + $attachment->delete(); + } + foreach ($account->transactions()->get() as $transaction) { + $transaction->delete(); + } + $account->notes()->delete(); + $account->locations()->delete(); + + } + +} diff --git a/app/Handlers/Observer/AttachmentObserver.php b/app/Handlers/Observer/AttachmentObserver.php new file mode 100644 index 0000000000..ccc003fb31 --- /dev/null +++ b/app/Handlers/Observer/AttachmentObserver.php @@ -0,0 +1,36 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Attachment; + +/** + * Class AttachmentObserver + */ +class AttachmentObserver +{ + public function deleting(Attachment $attachment): void + { + app('log')->debug('Observe "deleting" of an attachment.'); + $attachment->notes()->delete(); + } +} diff --git a/app/Handlers/Observer/BillObserver.php b/app/Handlers/Observer/BillObserver.php new file mode 100644 index 0000000000..81878cf586 --- /dev/null +++ b/app/Handlers/Observer/BillObserver.php @@ -0,0 +1,45 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Bill; + +/** + * Class BillObserver + */ +class BillObserver +{ + /** + * @param Bill $bill + * + * @return void + */ + public function deleting(Bill $bill): void + { + app('log')->debug('Observe "deleting" of a bill.'); + foreach ($bill->attachments()->get() as $attachment) { + $attachment->delete(); + } + $bill->notes()->delete(); + } + +} diff --git a/app/Handlers/Observer/BudgetObserver.php b/app/Handlers/Observer/BudgetObserver.php new file mode 100644 index 0000000000..06fee37946 --- /dev/null +++ b/app/Handlers/Observer/BudgetObserver.php @@ -0,0 +1,49 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Budget; + +/** + * Class BudgetObserver + */ +class BudgetObserver +{ + /** + * @param Budget $budget + * + * @return void + */ + public function deleting(Budget $budget): void + { + app('log')->debug('Observe "deleting" of a budget.'); + foreach ($budget->attachments()->get() as $attachment) { + $attachment->delete(); + } + + $budget->budgetlimits()->delete(); + + $budget->notes()->delete(); + $budget->autoBudgets()->delete(); + } + +} diff --git a/app/Handlers/Observer/CategoryObserver.php b/app/Handlers/Observer/CategoryObserver.php new file mode 100644 index 0000000000..1c0c2d22e8 --- /dev/null +++ b/app/Handlers/Observer/CategoryObserver.php @@ -0,0 +1,45 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Category; + +/** + * Class CategoryObserver + */ +class CategoryObserver +{ + /** + * @param Category $category + * + * @return void + */ + public function deleting(Category $category): void + { + app('log')->debug('Observe "deleting" of a category.'); + foreach ($category->attachments()->get() as $attachment) { + $attachment->delete(); + } + $category->notes()->delete(); + } + +} diff --git a/app/Handlers/Observer/PiggyBankObserver.php b/app/Handlers/Observer/PiggyBankObserver.php new file mode 100644 index 0000000000..e448e8378b --- /dev/null +++ b/app/Handlers/Observer/PiggyBankObserver.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\PiggyBankRepetition; + +/** + * Class PiggyBankObserver + */ +class PiggyBankObserver +{ + /** + * @param PiggyBank $piggyBank + * + * @return void + */ + public function created(PiggyBank $piggyBank): void + { + app('log')->debug('Observe "created" of a piggy bank.'); + $repetition = new PiggyBankRepetition(); + $repetition->piggyBank()->associate($piggyBank); + $repetition->startdate = $piggyBank->startdate; + $repetition->targetdate = $piggyBank->targetdate; + $repetition->currentamount = 0; + $repetition->save(); + } + + /** + * Also delete related objects. + * + * @param PiggyBank $piggyBank + * + * @return void + */ + public function deleting(PiggyBank $piggyBank): void + { + app('log')->debug('Observe "deleting" of a piggy bank.'); + + foreach ($piggyBank->attachments()->get() as $attachment) { + $attachment->delete(); + } + + $piggyBank->piggyBankEvents()->delete(); + $piggyBank->piggyBankRepetitions()->delete(); + + $piggyBank->notes()->delete(); + } + +} diff --git a/app/Handlers/Observer/RecurrenceObserver.php b/app/Handlers/Observer/RecurrenceObserver.php new file mode 100644 index 0000000000..b0357962c3 --- /dev/null +++ b/app/Handlers/Observer/RecurrenceObserver.php @@ -0,0 +1,52 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Recurrence; + +/** + * Class RecurrenceObserver + */ +class RecurrenceObserver +{ + /** + * @param Recurrence $recurrence + * + * @return void + */ + public function deleting(Recurrence $recurrence): void + { + app('log')->debug('Observe "deleting" of a recurrence.'); + foreach ($recurrence->attachments()->get() as $attachment) { + $attachment->delete(); + } + + $recurrence->recurrenceRepetitions()->delete(); + $recurrence->recurrenceMeta()->delete(); + foreach ($recurrence->recurrenceTransactions()->get() as $transaction) { + $transaction->delete(); + } + $recurrence->notes()->delete(); + + } + +} diff --git a/app/Handlers/Observer/RecurrenceTransactionObserver.php b/app/Handlers/Observer/RecurrenceTransactionObserver.php new file mode 100644 index 0000000000..874ac6bd26 --- /dev/null +++ b/app/Handlers/Observer/RecurrenceTransactionObserver.php @@ -0,0 +1,42 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\RecurrenceTransaction; + +/** + * Class RecurrenceTransactionObserver + */ +class RecurrenceTransactionObserver +{ + /** + * @param RecurrenceTransaction $transaction + * + * @return void + */ + public function deleting(RecurrenceTransaction $transaction): void + { + app('log')->debug('Observe "deleting" of a recurrence transaction.'); + $transaction->recurrenceTransactionMeta()->delete(); + } + +} diff --git a/app/Handlers/Observer/RuleGroupObserver.php b/app/Handlers/Observer/RuleGroupObserver.php new file mode 100644 index 0000000000..b53522f062 --- /dev/null +++ b/app/Handlers/Observer/RuleGroupObserver.php @@ -0,0 +1,45 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\RuleGroup; + +/** + * Class RuleGroupObserver + */ +class RuleGroupObserver +{ + /** + * @param RuleGroup $ruleGroup + * + * @return void + */ + public function deleting(RuleGroup $ruleGroup): void + { + app('log')->debug('Observe "deleting" of a rule group.'); + foreach ($ruleGroup->rules()->get() as $rule) { + $rule->delete(); + } + + } + +} diff --git a/app/Handlers/Observer/RuleObserver.php b/app/Handlers/Observer/RuleObserver.php new file mode 100644 index 0000000000..e372250ce7 --- /dev/null +++ b/app/Handlers/Observer/RuleObserver.php @@ -0,0 +1,43 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Rule; + +/** + * Class RuleObserver + */ +class RuleObserver +{ + /** + * @param Rule $rule + * + * @return void + */ + public function deleting(Rule $rule): void + { + app('log')->debug('Observe "deleting" of a rule.'); + $rule->ruleActions()->delete(); + $rule->ruleTriggers()->delete(); + } + +} diff --git a/app/Handlers/Observer/TagObserver.php b/app/Handlers/Observer/TagObserver.php new file mode 100644 index 0000000000..d9ed47195c --- /dev/null +++ b/app/Handlers/Observer/TagObserver.php @@ -0,0 +1,48 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Tag; + +/** + * Class TagObserver + */ +class TagObserver +{ + /** + * @param Tag $tag + * + * @return void + */ + public function deleting(Tag $tag): void + { + app('log')->debug('Observe "deleting" of a tag.'); + + foreach ($tag->attachments()->get() as $attachment) { + $attachment->delete(); + } + + $tag->locations()->delete(); + + } + +} diff --git a/app/Handlers/Observer/TransactionGroupObserver.php b/app/Handlers/Observer/TransactionGroupObserver.php new file mode 100644 index 0000000000..64e55f133b --- /dev/null +++ b/app/Handlers/Observer/TransactionGroupObserver.php @@ -0,0 +1,39 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\TransactionGroup; + +/** + * Class TransactionGroup + */ +class TransactionGroupObserver +{ + public function deleting(TransactionGroup $transactionGroup): void + { + app('log')->debug('Observe "deleting" of a transaction group.'); + foreach ($transactionGroup->transactionJournals()->get() as $journal) { + $journal->delete(); + } + } + +} diff --git a/app/Handlers/Observer/TransactionJournalObserver.php b/app/Handlers/Observer/TransactionJournalObserver.php new file mode 100644 index 0000000000..573154dcc2 --- /dev/null +++ b/app/Handlers/Observer/TransactionJournalObserver.php @@ -0,0 +1,55 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\TransactionJournal; + +/** + * Class TransactionJournalObserver + */ +class TransactionJournalObserver +{ + /** + * @param TransactionJournal $transactionJournal + * + * @return void + */ + public function deleting(TransactionJournal $transactionJournal): void + { + app('log')->debug('Observe "deleting" of a transaction journal.'); + + // to make sure the listener doesn't get back to use and loop + TransactionJournal::withoutEvents(function () use ($transactionJournal) { + foreach ($transactionJournal->transactions()->get() as $transaction) { + $transaction->delete(); + } + }); + foreach ($transactionJournal->attachments()->get() as $attachment) { + $attachment->delete(); + } + $transactionJournal->locations()->delete(); + $transactionJournal->sourceJournalLinks()->delete(); + $transactionJournal->destJournalLinks()->delete(); + $transactionJournal->auditLogEntries()->delete(); + } + +} diff --git a/app/Handlers/Observer/TransactionObserver.php b/app/Handlers/Observer/TransactionObserver.php new file mode 100644 index 0000000000..4ca87dc0e8 --- /dev/null +++ b/app/Handlers/Observer/TransactionObserver.php @@ -0,0 +1,37 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Transaction; + +/** + * Class TransactionObserver + */ +class TransactionObserver +{ + public function deleting(Transaction $transaction): void + { + app('log')->debug('Observe "deleting" of a transaction.'); + $transaction->transactionJournal->delete(); + } + +} diff --git a/app/Handlers/Observer/WebhookMessageObserver.php b/app/Handlers/Observer/WebhookMessageObserver.php new file mode 100644 index 0000000000..c566aaafb3 --- /dev/null +++ b/app/Handlers/Observer/WebhookMessageObserver.php @@ -0,0 +1,43 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Webhook; +use FireflyIII\Models\WebhookMessage; + +/** + * Class WebhookMessageObserver + */ +class WebhookMessageObserver +{ + + /** + * @param WebhookMessage $webhookMessage + * + * @return void + */ + public function deleting(WebhookMessage $webhookMessage): void + { + app('log')->debug('Observe "deleting" of a webhook message.'); + $webhookMessage->webhookAttempts()->delete(); + } +} diff --git a/app/Handlers/Observer/WebhookObserver.php b/app/Handlers/Observer/WebhookObserver.php new file mode 100644 index 0000000000..4a5f091a4c --- /dev/null +++ b/app/Handlers/Observer/WebhookObserver.php @@ -0,0 +1,44 @@ +. + */ + +namespace FireflyIII\Handlers\Observer; + +use FireflyIII\Models\Webhook; + +/** + * Class WebhookObserver + */ +class WebhookObserver +{ + /** + * @param Webhook $webhook + * + * @return void + */ + public function deleting(Webhook $webhook): void + { + app('log')->debug('Observe "deleting" of a webhook.'); + foreach ($webhook->webhookMessages() as $message) { + $message->delete(); + } + } + +} diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index 99d5678ecf..235a8c1663 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -154,7 +154,7 @@ class Attachment extends Model } /** - * Get all of the notes. + * Get all the notes. */ public function notes(): MorphMany { diff --git a/app/Models/Location.php b/app/Models/Location.php index 4894867837..76ea452351 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -97,7 +97,7 @@ class Location extends Model } /** - * Get all of the accounts. + * Get all the accounts. */ public function accounts(): MorphMany { @@ -105,7 +105,7 @@ class Location extends Model } /** - * Get all of the owning attachable models. + * Get all the owning attachable models. * * * @return MorphTo diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 2d8e44ea97..9581764f8e 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -201,6 +201,14 @@ class TransactionJournal extends Model return $this->morphMany(Attachment::class, 'attachable'); } + /** + * @return MorphMany + */ + public function auditLogEntries(): MorphMany + { + return $this->morphMany(AuditLogEntry::class, 'auditable'); + } + /** * @return BelongsTo */ @@ -254,7 +262,7 @@ class TransactionJournal extends Model } /** - * Get all of the notes. + * Get all the notes. */ public function notes(): MorphMany { diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 8ae53d9a93..ed7df41697 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -26,12 +26,13 @@ namespace FireflyIII\Providers; use FireflyIII\Events\ActuallyLoggedIn; use FireflyIII\Events\Admin\InvitationCreated; use FireflyIII\Events\AdminRequestedTestMessage; -use FireflyIII\Events\ChangedPiggyBankAmount; use FireflyIII\Events\DestroyedTransactionGroup; use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\Model\BudgetLimit\Created; use FireflyIII\Events\Model\BudgetLimit\Deleted; use FireflyIII\Events\Model\BudgetLimit\Updated; +use FireflyIII\Events\Model\PiggyBank\ChangedAmount; +use FireflyIII\Events\Model\PiggyBank\ChangedPiggyBankAmount; use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray; use FireflyIII\Events\Model\Rule\RuleActionFailedOnObject; use FireflyIII\Events\NewVersionAvailable; @@ -47,8 +48,38 @@ use FireflyIII\Events\UpdatedAccount; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Events\UserChangedEmail; use FireflyIII\Events\WarnUserAboutBill; +use FireflyIII\Handlers\Observer\AccountObserver; +use FireflyIII\Handlers\Observer\AttachmentObserver; +use FireflyIII\Handlers\Observer\BillObserver; +use FireflyIII\Handlers\Observer\BudgetObserver; +use FireflyIII\Handlers\Observer\CategoryObserver; +use FireflyIII\Handlers\Observer\PiggyBankObserver; +use FireflyIII\Handlers\Observer\RecurrenceObserver; +use FireflyIII\Handlers\Observer\RecurrenceTransactionObserver; +use FireflyIII\Handlers\Observer\RuleGroupObserver; +use FireflyIII\Handlers\Observer\RuleObserver; +use FireflyIII\Handlers\Observer\TagObserver; +use FireflyIII\Handlers\Observer\TransactionGroupObserver; +use FireflyIII\Handlers\Observer\TransactionJournalObserver; +use FireflyIII\Handlers\Observer\TransactionObserver; +use FireflyIII\Handlers\Observer\WebhookMessageObserver; +use FireflyIII\Handlers\Observer\WebhookObserver; +use FireflyIII\Models\Account; +use FireflyIII\Models\Attachment; +use FireflyIII\Models\Bill; +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; use FireflyIII\Models\PiggyBank; -use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Models\Recurrence; +use FireflyIII\Models\RecurrenceTransaction; +use FireflyIII\Models\Rule; +use FireflyIII\Models\RuleGroup; +use FireflyIII\Models\Tag; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\Webhook; +use FireflyIII\Models\WebhookMessage; use Illuminate\Auth\Events\Login; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Laravel\Passport\Events\AccessTokenCreated; @@ -158,9 +189,10 @@ class EventServiceProvider extends ServiceProvider 'FireflyIII\Handlers\Events\AuditEventHandler@storeAuditEvent', ], // piggy bank related events: - ChangedPiggyBankAmount::class => [ - 'FireflyIII\Handlers\Events\PiggyBankEventHandler@changePiggyAmount', + ChangedAmount::class => [ + 'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount', ], + // budget related events: CRUD budget limit Created::class => [ 'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@created', @@ -187,24 +219,30 @@ class EventServiceProvider extends ServiceProvider */ public function boot(): void { - parent::boot(); - $this->registerCreateEvents(); + $this->registerObservers(); } /** - * TODO needs a dedicated (static) method. + * @return void */ - protected function registerCreateEvents(): void + private function registerObservers(): void { - PiggyBank::created( - static function (PiggyBank $piggyBank) { - $repetition = new PiggyBankRepetition(); - $repetition->piggyBank()->associate($piggyBank); - $repetition->startdate = $piggyBank->startdate; - $repetition->targetdate = $piggyBank->targetdate; - $repetition->currentamount = 0; - $repetition->save(); - } - ); + app('log')->debug('Register observers'); + Attachment::observe(new AttachmentObserver()); + PiggyBank::observe(new PiggyBankObserver()); + Account::observe(new AccountObserver()); + Bill::observe(new BillObserver()); + Budget::observe(new BudgetObserver()); + Category::observe(new CategoryObserver()); + Recurrence::observe(new RecurrenceObserver()); + RecurrenceTransaction::observe(new RecurrenceTransactionObserver()); + Rule::observe(new RuleObserver()); + RuleGroup::observe(new RuleGroupObserver()); + Tag::observe(new TagObserver()); + Transaction::observe(new TransactionObserver()); + TransactionJournal::observe(new TransactionJournalObserver()); + TransactionGroup::observe(new TransactionGroupObserver()); + Webhook::observe(new WebhookObserver()); + WebhookMessage::observe(new WebhookMessageObserver()); } } diff --git a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index ca8e41335d..b8e8392e26 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -25,7 +25,8 @@ declare(strict_types=1); namespace FireflyIII\Repositories\PiggyBank; use Exception; -use FireflyIII\Events\ChangedPiggyBankAmount; +use FireflyIII\Events\Model\PiggyBank\ChangedAmount; +use FireflyIII\Events\Model\PiggyBank\ChangedPiggyBankAmount; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Note; use FireflyIII\Models\PiggyBank; @@ -80,7 +81,7 @@ trait ModifiesPiggyBanks $repetition->save(); Log::debug('addAmount [a]: Trigger change for negative amount.'); - event(new ChangedPiggyBankAmount($piggyBank, bcmul($amount, '-1'), $journal, null)); + event(new ChangedAmount($piggyBank, bcmul($amount, '-1'), $journal, null)); return true; } @@ -103,7 +104,7 @@ trait ModifiesPiggyBanks $repetition->save(); Log::debug('addAmount [b]: Trigger change for positive amount.'); - event(new ChangedPiggyBankAmount($piggyBank, $amount, $journal, null)); + event(new ChangedAmount($piggyBank, $amount, $journal, null)); return true; } @@ -202,11 +203,11 @@ trait ModifiesPiggyBanks if (-1 === bccomp($difference, '0')) { Log::debug('addAmount [c]: Trigger change for negative amount.'); - event(new ChangedPiggyBankAmount($piggyBank, $difference, null, null)); + event(new ChangedAmount($piggyBank, $difference, null, null)); } if (1 === bccomp($difference, '0')) { Log::debug('addAmount [d]: Trigger change for positive amount.'); - event(new ChangedPiggyBankAmount($piggyBank, $difference, null, null)); + event(new ChangedAmount($piggyBank, $difference, null, null)); } return $piggyBank; @@ -386,7 +387,7 @@ trait ModifiesPiggyBanks $difference = bcsub($piggyBank->targetamount, $repetition->currentamount); // an amount will be removed, create "negative" event: - event(new ChangedPiggyBankAmount($piggyBank, $difference, null, null)); + event(new ChangedAmount($piggyBank, $difference, null, null)); $repetition->currentamount = $piggyBank->targetamount; $repetition->save(); diff --git a/app/Repositories/UserGroup/UserGroupRepository.php b/app/Repositories/UserGroup/UserGroupRepository.php index 333800766b..7be53c78f8 100644 --- a/app/Repositories/UserGroup/UserGroupRepository.php +++ b/app/Repositories/UserGroup/UserGroupRepository.php @@ -25,12 +25,16 @@ declare(strict_types=1); namespace FireflyIII\Repositories\UserGroup; +use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\UserGroupFactory; use FireflyIII\Models\GroupMembership; use FireflyIII\Models\UserGroup; +use FireflyIII\Models\UserRole; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Collection; +use ValueError; /** * Class UserGroupRepository @@ -57,41 +61,36 @@ class UserGroupRepository implements UserGroupRepositoryInterface // user has memberships of other groups? $count = $user->groupMemberships()->where('user_group_id', '!=', $userGroup->id)->count(); if (0 === $count) { - app('log')->debug('User has no other memberships and needs a new administration.'); - // makeNewAdmin() - // assignToUser(). + app('log')->debug('User has no other memberships and needs a new user group.'); + $newUserGroup = $this->createNewUserGroup($user); + $user->user_group_id = $newUserGroup->id; + $user->save(); + app('log')->debug(sprintf('Make new group #%d ("%s")', $newUserGroup->id, $newUserGroup->title)); } // user has other memberships, select one at random and assign it to the user. if ($count > 0) { - // findAndAssign() + app('log')->debug('User has other memberships and will be assigned a new administration.'); + /** @var GroupMembership $first */ + $first = $user->groupMemberships()->where('user_group_id', '!=', $userGroup->id)->inRandomOrder()->first(); + $user->user_group_id = $first->id; + $user->save(); } - // deleteMembership() + // delete membership so group is empty after this for-loop. + $membership->delete(); } // all users are now moved away from user group. // time to DESTROY all objects. - // TODO piggy banks linked to accounts were deleting. - $userGroup->piggyBanks()->delete(); - $userGroup->accounts()->delete(); - $userGroup->availableBudgets()->delete(); - $userGroup->attachments()->delete(); - $userGroup->bills()->delete(); - $userGroup->budgets()->delete(); - $userGroup->categories()->delete(); - $userGroup->currencyExchangeRates()->delete(); - $userGroup->objectGroups()->delete(); - $userGroup->recurrences()->delete(); - $userGroup->rules()->delete(); - $userGroup->ruleGroups()->delete(); - $userGroup->tags()->delete(); - $userGroup->transactionJournals()->delete(); // TODO needs delete service probably. - $userGroup->transactionGroups()->delete(); // TODO needs delete service probably. - $userGroup->webhooks()->delete(); - - // user group deletion should also delete everything else. - // for all users, if this is the primary user group switch to the first alternative. - // if they have no other memberships, create a new user group for them. + // we have to do this one by one to trigger the necessary observers :( + $objects = ['availableBudgets', 'bills', 'budgets', 'categories', 'currencyExchangeRates', 'objectGroups', + 'recurrences', 'rules', 'ruleGroups', 'tags', 'transactionGroups', 'transactionJournals', 'piggyBanks', 'accounts', 'webhooks', + ]; + foreach ($objects as $object) { + foreach ($userGroup->$object()->get() as $item) { + $item->delete(); + } + } $userGroup->delete(); - + app('log')->debug('Done!'); } /** @@ -114,6 +113,59 @@ class UserGroupRepository implements UserGroupRepositoryInterface return $collection; } + /** + * Because there is the chance that a group with this name already exists, + * Firefly III runs a little loop of combinations to make sure the group name is unique. + * + * @param User $user + * + * @return UserGroup + */ + private function createNewUserGroup(User $user): UserGroup + { + $loop = 0; + $groupName = $user->email; + $exists = true; + $existingGroup = null; + while ($exists && $loop < 10) { + $existingGroup = $this->findByName($groupName); + if (null === $existingGroup) { + $exists = false; + $existingGroup = $this->store(['user' => $user, 'title' => $groupName]); + } + if (null !== $existingGroup) { + // group already exists + $groupName = sprintf('%s-%s', $user->email, substr(sha1((string)(rand(1000, 9999) . microtime())), 0, 4)); + } + $loop++; + } + return $existingGroup; + } + + /** + * @param string $title + * + * @return UserGroup|null + */ + public function findByName(string $title): ?UserGroup + { + return UserGroup::whereTitle($title)->first(); + } + + /** + * @param array $data + * + * @return UserGroup + * @throws FireflyException + */ + public function store(array $data): UserGroup + { + $data['user'] = $this->user; + /** @var UserGroupFactory $factory */ + $factory = app(UserGroupFactory::class); + return $factory->create($data); + } + /** * Returns all groups. * @@ -136,15 +188,98 @@ class UserGroupRepository implements UserGroupRepositoryInterface } /** - * @param array $data - * - * @return UserGroup + * @inheritDoc */ - public function store(array $data): UserGroup + public function update(UserGroup $userGroup, array $data): UserGroup { - $data['user'] = $this->user; - /** @var UserGroupFactory $factory */ - $factory = app(UserGroupFactory::class); - return $factory->create($data); + $userGroup->title = $data['title']; + $userGroup->save(); + return $userGroup; + } + + /** + * @inheritDoc + * @throws FireflyException + */ + public function updateMembership(UserGroup $userGroup, array $data): UserGroup + { + $owner = UserRole::whereTitle(UserRoleEnum::OWNER)->first(); + app('log')->debug('in update membership'); + $user = null; + if (array_key_exists('id', $data)) { + $user = User::find($data['id']); + app('log')->debug('Found user by ID'); + } + if (array_key_exists('email', $data) && '' !== (string)$data['email']) { + $user = User::whereEmail($data['email'])->first(); + app('log')->debug('Found user by email'); + } + if (null === $user) { + // should throw error, but validator already catches this. + app('log')->debug('No user found'); + return $userGroup; + } + // count the number of members in the group right now: + $membershipCount = $userGroup->groupMemberships()->distinct()->get(['group_memberships.user_id'])->count(); + + // if it's 1: + if (1 === $membershipCount) { + $lastUserId = (int)$userGroup->groupMemberships()->distinct()->first(['group_memberships.user_id'])->user_id; + // if this is also the user we're editing right now, and we remove all of their roles: + if ($lastUserId === (int)$user->id && 0 === count($data['roles'])) { + app('log')->debug('User is last in this group, refuse to act'); + throw new FireflyException('You cannot remove the last member from this user group. Delete the user group instead.'); + } + // if this is also the user we're editing right now, and do not grant them the owner role: + if ($lastUserId === (int)$user->id && count($data['roles']) > 0 && !in_array(UserRoleEnum::OWNER->value, $data['roles'], true)) { + app('log')->debug('User needs to have owner role in this group, refuse to act'); + throw new FireflyException('The last member in this user group must get or keep the "owner" role.'); + } + } + if ($membershipCount > 1) { + // group has multiple members. How many are owner, except the user we're editing now? + $ownerCount = $userGroup->groupMemberships() + ->where('user_role_id', $owner->id) + ->where('user_id', '!=', $user->id)->count(); + // if there are no other owners and the current users does not get or keep the owner role, refuse. + if (0 === $ownerCount && (0 === count($data['roles']) || (count($data['roles']) > 0 && !in_array(UserRoleEnum::OWNER->value, $data['roles'], true)))) { + app('log')->debug('User needs to keep owner role in this group, refuse to act'); + throw new FireflyException('The last owner in this user group must keep the "owner" role.'); + } + } + // simplify the list of roles: + $rolesSimplified = $this->simplifyListByName($data['roles']); + + // delete all existing roles for user: + $user->groupMemberships()->where('user_group_id', $userGroup->id)->delete(); + foreach ($rolesSimplified as $role) { + try { + $enum = UserRoleEnum::from($role); + } catch (ValueError $e) { + // TODO error message + continue; + } + $userRole = UserRole::whereTitle($enum->value)->first(); + $user->groupMemberships()->create(['user_group_id' => $userGroup->id, 'user_role_id' => $userRole->id]); + } + return $userGroup; + } + + /** + * @param array $roles + * + * @return array + */ + private function simplifyListByName(array $roles): array + { + if (in_array(UserRoleEnum::OWNER->value, $roles, true)) { + app('log')->debug(sprintf('List of roles is [%1$s] but this includes "%2$s", so return [%2$s]', join(',', $roles), UserRoleEnum::OWNER->value)); + return [UserRoleEnum::OWNER->value]; + } + if (in_array(UserRoleEnum::FULL->value, $roles, true)) { + app('log')->debug(sprintf('List of roles is [%1$s] but this includes "%2$s", so return [%2$s]', join(',', $roles), UserRoleEnum::FULL->value)); + return [UserRoleEnum::FULL->value]; + } + return $roles; } } diff --git a/app/Repositories/UserGroup/UserGroupRepositoryInterface.php b/app/Repositories/UserGroup/UserGroupRepositoryInterface.php index f703d917a0..0cc3bf1cf6 100644 --- a/app/Repositories/UserGroup/UserGroupRepositoryInterface.php +++ b/app/Repositories/UserGroup/UserGroupRepositoryInterface.php @@ -58,4 +58,27 @@ interface UserGroupRepositoryInterface * @return void */ public function setUser(User | Authenticatable | null $user): void; + + /** + * @param array $data + * + * @return UserGroup + */ + public function store(array $data): UserGroup; + + /** + * @param UserGroup $userGroup + * @param array $data + * + * @return UserGroup + */ + public function update(UserGroup $userGroup, array $data): UserGroup; + + /** + * @param UserGroup $userGroup + * @param array $data + * + * @return UserGroup + */ + public function updateMembership(UserGroup $userGroup, array $data): UserGroup; } diff --git a/app/Support/Request/ChecksLogin.php b/app/Support/Request/ChecksLogin.php index 70e69e9281..2839ceea86 100644 --- a/app/Support/Request/ChecksLogin.php +++ b/app/Support/Request/ChecksLogin.php @@ -23,7 +23,11 @@ declare(strict_types=1); namespace FireflyIII\Support\Request; +use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Models\UserGroup; +use FireflyIII\User; use Illuminate\Support\Facades\Log; +use ValueError; /** * Trait ChecksLogin @@ -38,7 +42,29 @@ trait ChecksLogin public function authorize(): bool { Log::debug(sprintf('Now in %s', __METHOD__)); - // Only allow logged in users - return auth()->check(); + // Only allow logged-in users + $check = auth()->check(); + if (!$check) { + return false; + } + if (!property_exists($this, 'acceptedRoles')) { + app('log')->debug('Request class has no acceptedRoles array'); + return true; // check for false already took place. + } + /** @var UserGroup $userGroup */ + $userGroup = $this->route()->parameter('userGroup'); + if (null === $userGroup) { + app('log')->debug('Request class has no userGroup parameter.'); + return true; + } + /** @var User $user */ + $user = auth()->user(); + /** @var UserRoleEnum $role */ + foreach ($this->acceptedRoles as $role) { + if ($user->hasRoleInGroup($userGroup, $role, true, true)) { + return true; + } + } + return false; } } diff --git a/routes/api.php b/routes/api.php index e7ea26fe69..175bac9e43 100644 --- a/routes/api.php +++ b/routes/api.php @@ -215,6 +215,8 @@ Route::group( Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']); Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); Route::get('{userGroup}', ['uses' => 'ShowController@show', 'as' => 'show']); + Route::put('{userGroup}', ['uses' => 'UpdateController@update', 'as' => 'update']); + Route::put('{userGroup}/update-membership', ['uses' => 'UpdateController@updateMembership', 'as' => 'updateMembership']); Route::delete('{userGroup}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); } );