diff --git a/app/Api/V2/Controllers/Model/Transaction/StoreController.php b/app/Api/V2/Controllers/Model/Transaction/StoreController.php index 2b0982ff2e..91567dd1a3 100644 --- a/app/Api/V2/Controllers/Model/Transaction/StoreController.php +++ b/app/Api/V2/Controllers/Model/Transaction/StoreController.php @@ -32,6 +32,10 @@ use Illuminate\Http\JsonResponse; class StoreController extends Controller { /** + * TODO this method is practically the same as the V1 method and borrows as much code as possible. + * TODO still it duplicates a lot. + * TODO the v1 endpoints will never support separate administrations, this is an important distinction. + * * @return JsonResponse */ public function post(): JsonResponse diff --git a/app/Api/V2/Controllers/NetWorthController.php b/app/Api/V2/Controllers/Summary/NetWorthController.php similarity index 93% rename from app/Api/V2/Controllers/NetWorthController.php rename to app/Api/V2/Controllers/Summary/NetWorthController.php index 0679eb561d..25c1889aa2 100644 --- a/app/Api/V2/Controllers/NetWorthController.php +++ b/app/Api/V2/Controllers/Summary/NetWorthController.php @@ -2,7 +2,7 @@ /* * NetWorthController.php - * Copyright (c) 2022 james@firefly-iii.org + * Copyright (c) 2023 james@firefly-iii.org * * This file is part of Firefly III (https://github.com/firefly-iii). * @@ -22,8 +22,9 @@ declare(strict_types=1); -namespace FireflyIII\Api\V2\Controllers; +namespace FireflyIII\Api\V2\Controllers\Summary; +use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Request\Generic\SingleDateRequest; use FireflyIII\Helpers\Report\NetWorthInterface; use Illuminate\Http\JsonResponse; diff --git a/app/Api/V2/Controllers/VersionUpdateController.php b/app/Api/V2/Controllers/System/VersionUpdateController.php similarity index 86% rename from app/Api/V2/Controllers/VersionUpdateController.php rename to app/Api/V2/Controllers/System/VersionUpdateController.php index 0852ae7d74..35d4b3fb3f 100644 --- a/app/Api/V2/Controllers/VersionUpdateController.php +++ b/app/Api/V2/Controllers/System/VersionUpdateController.php @@ -2,7 +2,7 @@ /* * VersionUpdateController.php - * Copyright (c) 2022 james@firefly-iii.org + * Copyright (c) 2023 james@firefly-iii.org * * This file is part of Firefly III (https://github.com/firefly-iii). * @@ -22,7 +22,9 @@ declare(strict_types=1); -namespace FireflyIII\Api\V2\Controllers; +namespace FireflyIII\Api\V2\Controllers\System; + +use FireflyIII\Api\V2\Controllers\Controller; /** * Class VersionUpdateController diff --git a/app/Api/V2/Controllers/Transaction/List/AccountController.php b/app/Api/V2/Controllers/Transaction/List/AccountController.php index d0ce8d858f..0553399e40 100644 --- a/app/Api/V2/Controllers/Transaction/List/AccountController.php +++ b/app/Api/V2/Controllers/Transaction/List/AccountController.php @@ -2,7 +2,7 @@ /* * AccountController.php - * Copyright (c) 2022 james@firefly-iii.org + * Copyright (c) 2023 james@firefly-iii.org * * This file is part of Firefly III (https://github.com/firefly-iii). * diff --git a/app/Api/V2/Controllers/Transaction/List/TransactionController.php b/app/Api/V2/Controllers/Transaction/List/TransactionController.php index 07284a20d3..e863ae45f1 100644 --- a/app/Api/V2/Controllers/Transaction/List/TransactionController.php +++ b/app/Api/V2/Controllers/Transaction/List/TransactionController.php @@ -1,6 +1,4 @@ . */ +declare(strict_types=1); + namespace FireflyIII\Api\V2\Controllers\Transaction\List; use FireflyIII\Api\V2\Controllers\Controller; diff --git a/app/Api/V2/Controllers/Transaction/Sum/BillController.php b/app/Api/V2/Controllers/Transaction/Sum/BillController.php index bd031e34dc..fc976b62f5 100644 --- a/app/Api/V2/Controllers/Transaction/Sum/BillController.php +++ b/app/Api/V2/Controllers/Transaction/Sum/BillController.php @@ -2,7 +2,7 @@ /* * BillController.php - * Copyright (c) 2022 james@firefly-iii.org + * Copyright (c) 2023 james@firefly-iii.org * * This file is part of Firefly III (https://github.com/firefly-iii). * diff --git a/app/Api/V2/Controllers/UserGroup/DestroyController.php b/app/Api/V2/Controllers/UserGroup/DestroyController.php new file mode 100644 index 0000000000..5860ccc255 --- /dev/null +++ b/app/Api/V2/Controllers/UserGroup/DestroyController.php @@ -0,0 +1,32 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers\UserGroup; + +use FireflyIII\Api\V2\Controllers\Controller; + +/** + * Class DestroyController + */ +class DestroyController extends Controller +{ + +} diff --git a/app/Api/V2/Controllers/UserGroup/ShowController.php b/app/Api/V2/Controllers/UserGroup/ShowController.php new file mode 100644 index 0000000000..f254672d7b --- /dev/null +++ b/app/Api/V2/Controllers/UserGroup/ShowController.php @@ -0,0 +1,97 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers\UserGroup; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Models\UserGroup; +use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface; +use FireflyIII\Transformers\V2\UserGroupTransformer; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; + +/** + * Class ShowController + */ +class ShowController extends Controller +{ + private UserGroupRepositoryInterface $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->repository = app(UserGroupRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @param Request $request + * + * @return JsonResponse + */ + public function index(Request $request): JsonResponse + { + $collection = new Collection(); + // if the user has the system owner role, get all. Otherwise, get only the users' groups. + if (!auth()->user()->hasRole('owner')) { + $collection = $this->repository->get(); + } + if (auth()->user()->hasRole('owner')) { + $collection = $this->repository->getAll(); + } + $count = $collection->count(); + $userGroups = $collection->slice(($this->parameters->get('page') - 1) * $this->pageSize, $this->pageSize); + + $paginator = new LengthAwarePaginator($userGroups, $count, $this->pageSize, $this->parameters->get('page')); + $transformer = new UserGroupTransformer(); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList('user-groups', $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); + } + + /** + * @param Request $request + * @param UserGroup $userGroup + * + * @return JsonResponse + */ + public function show(Request $request, UserGroup $userGroup): JsonResponse + { + $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/Controllers/UserGroup/StoreController.php b/app/Api/V2/Controllers/UserGroup/StoreController.php new file mode 100644 index 0000000000..678e1946d0 --- /dev/null +++ b/app/Api/V2/Controllers/UserGroup/StoreController.php @@ -0,0 +1,67 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers\UserGroup; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Api\V2\Request\UserGroup\StoreRequest; +use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface; +use FireflyIII\Transformers\V2\UserGroupTransformer; +use Illuminate\Http\JsonResponse; + +/** + * Class StoreController + */ +class StoreController extends Controller +{ + private UserGroupRepositoryInterface $repository; + + /** + * + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->repository = app(UserGroupRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @return JsonResponse + */ + public function store(StoreRequest $request): JsonResponse + { + $all = $request->getAll(); + $userGroup = $this->repository->store($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/Controllers/UserGroup/UpdateController.php b/app/Api/V2/Controllers/UserGroup/UpdateController.php new file mode 100644 index 0000000000..97313b03ee --- /dev/null +++ b/app/Api/V2/Controllers/UserGroup/UpdateController.php @@ -0,0 +1,31 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers\UserGroup; + +use FireflyIII\Api\V2\Controllers\Controller; + +class UpdateController extends Controller +{ + + // basic edit van group + // add user, add rights, remove user, remove rights. +} diff --git a/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php b/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php index 7bd383b8a1..8a73be5eac 100644 --- a/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php +++ b/app/Api/V2/Request/Autocomplete/AutocompleteRequest.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V2\Request\Autocomplete; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\AccountType; use FireflyIII\Models\UserRole; @@ -92,7 +93,7 @@ class AutocompleteRequest extends FormRequest $validator->after( function (Validator $validator) { // validate if the account can access this administration - $this->validateAdministration($validator, [UserRole::CHANGE_TRANSACTIONS]); + $this->validateAdministration($validator, [UserRoleEnum::MANAGE_TRANSACTIONS]); } ); } diff --git a/app/Api/V2/Request/UserGroup/StoreRequest.php b/app/Api/V2/Request/UserGroup/StoreRequest.php new file mode 100644 index 0000000000..44a9db247a --- /dev/null +++ b/app/Api/V2/Request/UserGroup/StoreRequest.php @@ -0,0 +1,52 @@ +. + */ + +namespace FireflyIII\Api\V2\Request\UserGroup; + +use FireflyIII\Support\Request\ChecksLogin; +use FireflyIII\Support\Request\ConvertsDataTypes; +use Illuminate\Foundation\Http\FormRequest; + +class StoreRequest extends FormRequest +{ + use ChecksLogin; + use ConvertsDataTypes; + + /** + * @return array + */ + public function getAll(): array + { + return [ + 'title' => $this->convertString('title'), + ]; + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'title' => 'unique:user_groups,title|required|min:2|max:255', + ]; + } +} diff --git a/app/Console/Commands/Integrity/CreateGroupMemberships.php b/app/Console/Commands/Integrity/CreateGroupMemberships.php index a883aca285..5351c496f9 100644 --- a/app/Console/Commands/Integrity/CreateGroupMemberships.php +++ b/app/Console/Commands/Integrity/CreateGroupMemberships.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands\Integrity; use FireflyIII\Console\Commands\ShowsFriendlyMessages; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\GroupMembership; use FireflyIII\Models\UserGroup; @@ -85,7 +86,7 @@ class CreateGroupMemberships extends Command $userGroup = UserGroup::create(['title' => $user->email]); } - $userRole = UserRole::where('title', UserRole::OWNER)->first(); + $userRole = UserRole::where('title', UserRoleEnum::OWNER->value)->first(); if (null === $userRole) { throw new FireflyException('Firefly III could not find a user role. Please make sure all migrations have run.'); diff --git a/app/Enums/UserRoleEnum.php b/app/Enums/UserRoleEnum.php index 93beb532f1..af98413c55 100644 --- a/app/Enums/UserRoleEnum.php +++ b/app/Enums/UserRoleEnum.php @@ -29,12 +29,34 @@ namespace FireflyIII\Enums; */ enum UserRoleEnum: string { - case CHANGE_PIGGY_BANKS = 'change_piggies'; - case CHANGE_REPETITIONS = 'change_reps'; - case CHANGE_RULES = 'change_rules'; - case CHANGE_TRANSACTIONS = 'change_tx'; - case FULL = 'full'; - case OWNER = 'owner'; - case READ_ONLY = 'ro'; - case VIEW_REPORTS = 'view_reports'; + // most basic rights, cannot see other members, can see everything else. + case READ_ONLY = 'ro'; + + // required to even USE the group properly (in this order) + case MANAGE_TRANSACTIONS = 'mng_trx'; + + // required to edit, add or change categories/tags/object-groups + case MANAGE_META = 'mng_meta'; + + // manage other financial objects: + case MANAGE_BUDGETS = 'mng_budgets'; + case MANAGE_PIGGY_BANKS = 'mng_piggies'; + case MANAGE_REPETITIONS = 'mng_reps'; + case MANAGE_SUBSCRIPTIONS = 'mng_subscriptions'; + case MANAGE_RULES = 'mng_rules'; + case MANAGE_RECURRING = 'mng_recurring'; + case MANAGE_WEBHOOKS = 'mng_webhooks'; + case MANAGE_CURRENCIES = 'mng_currencies'; + + // view and generate reports + case VIEW_REPORTS = 'view_reports'; + + // view memberships. needs FULL to manage them. + case VIEW_MEMBERSHIPS = 'view_memberships'; + + // everything the creator can, except remove/change original creator and delete group + case FULL = 'full'; + + // reserved for original creator + case OWNER = 'owner'; } diff --git a/app/Factory/UserGroupFactory.php b/app/Factory/UserGroupFactory.php new file mode 100644 index 0000000000..18be0bc917 --- /dev/null +++ b/app/Factory/UserGroupFactory.php @@ -0,0 +1,62 @@ +. + */ + +namespace FireflyIII\Factory; + +use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\GroupMembership; +use FireflyIII\Models\UserGroup; +use FireflyIII\Models\UserRole; + +/** + * Class UserGroupFactory + */ +class UserGroupFactory +{ + /** + * @param array $data + * + * @return UserGroup + * @throws FireflyException + */ + public function create(array $data): UserGroup + { + $userGroup = new UserGroup(); + $userGroup->title = $data['title']; + $userGroup->save(); + + // grab the OWNER role: + $role = UserRole::whereTitle(UserRoleEnum::OWNER->value)->first(); + if (null === $role) { + throw new FireflyException('Role "owner" does not exist.'); + } + // make user member: + $groupMembership = new GroupMembership(); + $groupMembership->user_group_id = $userGroup->id; + $groupMembership->user_id = $data['user']->id; + $groupMembership->user_role_id = $role->id; + $groupMembership->save(); + + return $userGroup; + } + +} diff --git a/app/Handlers/Events/AutomationHandler.php b/app/Handlers/Events/AutomationHandler.php index 83ee2d2783..defb50f90f 100644 --- a/app/Handlers/Events/AutomationHandler.php +++ b/app/Handlers/Events/AutomationHandler.php @@ -48,18 +48,23 @@ class AutomationHandler public function reportJournals(RequestedReportOnJournals $event): void { Log::debug('In reportJournals.'); - $sendReport = config('firefly.send_report_journals'); - if (false === $sendReport) { - return; - } - /** @var UserRepositoryInterface $repository */ $repository = app(UserRepositoryInterface::class); $user = $repository->find($event->userId); - if (null === $user || 0 === $event->groups->count()) { + $sendReport = app('preferences')->getForUser($user, 'notification_transaction_creation', false)->data; + + if (false === $sendReport) { + Log::debug('Not sending report, because config says so.'); return; } + + if (null === $user || 0 === $event->groups->count()) { + Log::debug('No transaction groups in event, nothing to email about.'); + return; + } + Log::debug('Continue with message!'); + // transform groups into array: /** @var TransactionGroupTransformer $transformer */ $transformer = app(TransactionGroupTransformer::class); @@ -83,5 +88,6 @@ class AutomationHandler Log::error($e->getMessage()); Log::error($e->getTraceAsString()); } + Log::debug('If there is no error above this line, message was sent.'); } } diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 79e8d50c07..6f534d04a1 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -26,6 +26,7 @@ namespace FireflyIII\Handlers\Events; use Carbon\Carbon; use Database\Seeders\ExchangeRateSeeder; use Exception; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Events\ActuallyLoggedIn; use FireflyIII\Events\Admin\InvitationCreated; use FireflyIII\Events\DetectedNewIPAddress; @@ -144,7 +145,7 @@ class UserEventHandler } } /** @var UserRole|null $role */ - $role = UserRole::where('title', UserRole::OWNER)->first(); + $role = UserRole::where('title', UserRoleEnum::OWNER->value)->first(); if (null === $role) { throw new FireflyException('The user role is unexpectedly empty. Did you run all migrations?'); } diff --git a/app/Http/Requests/AccountFormRequest.php b/app/Http/Requests/AccountFormRequest.php index c669846070..95896d7579 100644 --- a/app/Http/Requests/AccountFormRequest.php +++ b/app/Http/Requests/AccountFormRequest.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Requests; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\Account; use FireflyIII\Models\Location; use FireflyIII\Models\UserRole; @@ -152,7 +153,7 @@ class AccountFormRequest extends FormRequest $validator->after( function (Validator $validator) { // validate if the account can access this administration - $this->validateAdministration($validator, [UserRole::CHANGE_TRANSACTIONS]); + $this->validateAdministration($validator, [UserRoleEnum::MANAGE_TRANSACTIONS->value]); } ); } diff --git a/app/Jobs/CreateRecurringTransactions.php b/app/Jobs/CreateRecurringTransactions.php index 20c81942f6..ec00f99e90 100644 --- a/app/Jobs/CreateRecurringTransactions.php +++ b/app/Jobs/CreateRecurringTransactions.php @@ -460,16 +460,18 @@ class CreateRecurringTransactions implements ShouldQueue $total = $this->repository->totalTransactions($recurrence, $repetition); $count = $this->repository->getJournalCount($recurrence) + 1; $transactions = $recurrence->recurrenceTransactions()->get(); - $return = []; + /** @var RecurrenceTransaction $first */ + $first = $transactions->first(); + $return = []; /** @var RecurrenceTransaction $transaction */ foreach ($transactions as $index => $transaction) { $single = [ - 'type' => strtolower($recurrence->transactionType->type), + 'type' => null === $first->transactionType ? strtolower($recurrence->transactionType->type) : strtolower($first->transactionType->type), 'date' => $date, 'user' => $recurrence->user_id, 'currency_id' => (int)$transaction->transaction_currency_id, 'currency_code' => null, - 'description' => $transactions->first()->description, + 'description' => $first->description, 'amount' => $transaction->amount, 'budget_id' => $this->repository->getBudget($transaction), 'budget_name' => null, diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index f4d08b1e31..20e68bfd51 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -25,23 +25,26 @@ declare(strict_types=1); namespace FireflyIII\Models; use Eloquent; +use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Support\Carbon; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class UserGroup * - * @property int $id - * @property Carbon|null $created_at - * @property Carbon|null $updated_at - * @property string|null $deleted_at - * @property string $title - * @property-read Collection|GroupMembership[] $groupMemberships - * @property-read int|null $group_memberships_count + * @property int $id + * @property Carbon|null $created_at + * @property Carbon|null $updated_at + * @property string|null $deleted_at + * @property string $title + * @property-read Collection|GroupMembership[] $groupMemberships + * @property-read int|null $group_memberships_count * @method static Builder|UserGroup newModelQuery() * @method static Builder|UserGroup newQuery() * @method static Builder|UserGroup query() @@ -50,24 +53,52 @@ use Illuminate\Support\Carbon; * @method static Builder|UserGroup whereId($value) * @method static Builder|UserGroup whereTitle($value) * @method static Builder|UserGroup whereUpdatedAt($value) - * @property-read Collection $accounts - * @property-read int|null $accounts_count - * @property-read Collection $availableBudgets - * @property-read int|null $available_budgets_count - * @property-read Collection $bills - * @property-read int|null $bills_count - * @property-read Collection $budgets - * @property-read int|null $budgets_count - * @property-read Collection $piggyBanks - * @property-read int|null $piggy_banks_count - * @property-read Collection $transactionJournals - * @property-read int|null $transaction_journals_count + * @property-read Collection $accounts + * @property-read int|null $accounts_count + * @property-read Collection $availableBudgets + * @property-read int|null $available_budgets_count + * @property-read Collection $bills + * @property-read int|null $bills_count + * @property-read Collection $budgets + * @property-read int|null $budgets_count + * @property-read Collection $piggyBanks + * @property-read int|null $piggy_banks_count + * @property-read Collection $transactionJournals + * @property-read int|null $transaction_journals_count * @mixin Eloquent */ class UserGroup extends Model { protected $fillable = ['title']; + /** + * Route binder. Converts the key in the URL to the specified object (or throw 404). + * + * @param string $value + * + * @return UserGroup + * @throws NotFoundHttpException + */ + public static function routeBinder(string $value): UserGroup + { + if (auth()->check()) { + $userGroupId = (int)$value; + /** @var User $user */ + $user = auth()->user(); + /** @var UserGroup $userGroup */ + $userGroup = UserGroup::find($userGroupId); + if (null === $userGroup) { + throw new NotFoundHttpException(); + } + // need at least ready only to be aware of the user group's existence, + // but owner/full role (in the group) or global owner role may overrule this. + if ($user->hasRoleInGroup($userGroup, UserRoleEnum::READ_ONLY, true, true)) { + return $userGroup; + } + } + throw new NotFoundHttpException(); + } + /** * Link to accounts. * diff --git a/app/Models/UserRole.php b/app/Models/UserRole.php index da88f6987b..5dbfa9b27c 100644 --- a/app/Models/UserRole.php +++ b/app/Models/UserRole.php @@ -53,18 +53,6 @@ use Illuminate\Support\Carbon; */ class UserRole extends Model { - public const CHANGE_PIGGY_BANKS = 'change_piggies'; - public const CHANGE_REPETITIONS = 'change_reps'; - public const CHANGE_RULES = 'change_rules'; - public const CHANGE_TRANSACTIONS = 'change_tx'; - public const FULL = 'full'; - public const MANAGE_CURRENCIES = 'manage_currencies'; - public const MANAGE_WEBHOOKS = 'manage_webhooks'; - public const OWNER = 'owner'; - public const READ_ONLY = 'ro'; - public const VIEW_REPORTS = 'view_reports'; - - protected $fillable = ['title']; /** diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index c8b4d42d8a..d4accbf037 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -47,6 +47,8 @@ use FireflyIII\Repositories\TransactionType\TransactionTypeRepository; use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface; use FireflyIII\Repositories\User\UserRepository; use FireflyIII\Repositories\User\UserRepositoryInterface; +use FireflyIII\Repositories\UserGroup\UserGroupRepository; +use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface; use FireflyIII\Repositories\Webhook\WebhookRepository; use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface; use FireflyIII\Services\FireflyIIIOrg\Update\UpdateRequest; @@ -212,6 +214,19 @@ class FireflyServiceProvider extends ServiceProvider } ); + $this->app->bind( + UserGroupRepositoryInterface::class, + static function (Application $app) { + /** @var UserGroupRepository $repository */ + $repository = app(UserGroupRepository::class); + if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth) + $repository->setUser(auth()->user()); + } + + return $repository; + } + ); + // more generators: $this->app->bind(PopupReportInterface::class, PopupReport::class); $this->app->bind(ReportHelperInterface::class, ReportHelper::class); diff --git a/app/Repositories/UserGroup/UserGroupRepository.php b/app/Repositories/UserGroup/UserGroupRepository.php new file mode 100644 index 0000000000..1d396b0497 --- /dev/null +++ b/app/Repositories/UserGroup/UserGroupRepository.php @@ -0,0 +1,91 @@ +. + */ + +namespace FireflyIII\Repositories\UserGroup; + +use FireflyIII\Factory\UserGroupFactory; +use FireflyIII\Models\GroupMembership; +use FireflyIII\Models\UserGroup; +use FireflyIII\User; +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Support\Collection; + +/** + * Class UserGroupRepository + */ +class UserGroupRepository implements UserGroupRepositoryInterface +{ + private User $user; + + /** + * Returns all groups the user is member in. + * + * @inheritDoc + */ + public function get(): Collection + { + $collection = new Collection(); + $memberships = $this->user->groupMemberships()->get(); + /** @var GroupMembership $membership */ + foreach ($memberships as $membership) { + /** @var UserGroup $group */ + $group = $membership->userGroup()->first(); + if (null !== $group) { + $collection->push($group); + } + } + return $collection; + } + + /** + * Returns all groups. + * + * @inheritDoc + */ + public function getAll(): Collection + { + return UserGroup::all(); + } + + /** + * @inheritDoc + */ + public function setUser(Authenticatable | User | null $user): void + { + app('log')->debug(sprintf('Now in %s', __METHOD__)); + if (null !== $user) { + $this->user = $user; + } + } + + /** + * @param array $data + * + * @return UserGroup + */ + public function store(array $data): UserGroup + { + $data['user'] = $this->user; + /** @var UserGroupFactory $factory */ + $factory = app(UserGroupFactory::class); + return $factory->create($data); + } +} diff --git a/app/Repositories/UserGroup/UserGroupRepositoryInterface.php b/app/Repositories/UserGroup/UserGroupRepositoryInterface.php new file mode 100644 index 0000000000..1a2ad681ad --- /dev/null +++ b/app/Repositories/UserGroup/UserGroupRepositoryInterface.php @@ -0,0 +1,49 @@ +. + */ + +namespace FireflyIII\Repositories\UserGroup; + +use FireflyIII\User; +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Support\Collection; + +/** + * Interface UserGroupRepositoryInterface + */ +interface UserGroupRepositoryInterface +{ + /** + * @return Collection + */ + public function get(): Collection; + + /** + * @return Collection + */ + public function getAll(): Collection; + + /** + * @param User|Authenticatable|null $user + * + * @return void + */ + public function setUser(User | Authenticatable | null $user): void; +} diff --git a/app/Transformers/V2/UserGroupTransformer.php b/app/Transformers/V2/UserGroupTransformer.php new file mode 100644 index 0000000000..6e72e221fa --- /dev/null +++ b/app/Transformers/V2/UserGroupTransformer.php @@ -0,0 +1,93 @@ +. + */ + +namespace FireflyIII\Transformers\V2; + +use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Models\GroupMembership; +use FireflyIII\Models\UserGroup; +use FireflyIII\User; +use Illuminate\Support\Collection; + +/** + * Class UserGroupTransformer + */ +class UserGroupTransformer extends AbstractTransformer +{ + private array $memberships; + + /** + * + */ + public function __construct() + { + $this->memberships = []; + } + + /** + * @inheritDoc + */ + public function collectMetaData(Collection $objects): void + { + if (auth()->check()) { + // collect memberships so they can be listed in the group. + /** @var User $user */ + $user = auth()->user(); + /** @var UserGroup $userGroup */ + foreach ($objects as $userGroup) { + $userGroupId = (int)$userGroup->id; + $access = $user->hasRoleInGroup($userGroup, UserRoleEnum::VIEW_MEMBERSHIPS, true, true); + if ($access) { + $groupMemberships = $userGroup->groupMemberships()->get(); + /** @var GroupMembership $groupMembership */ + foreach ($groupMemberships as $groupMembership) { + $this->memberships[$userGroupId][] = [ + 'user_id' => (string)$groupMembership->user_id, + 'user_email' => $groupMembership->user->email, + 'role' => $groupMembership->userRole->title, + ]; + } + } + } + } + } + + /** + * Transform the user group. + * + * @param UserGroup $userGroup + * + * @return array + */ + public function transform(UserGroup $userGroup): array + { + $return = [ + 'id' => (int)$userGroup->id, + 'created_at' => $userGroup->created_at->toAtomString(), + 'updated_at' => $userGroup->updated_at->toAtomString(), + 'title' => $userGroup->title, + 'members' => $this->memberships[(int)$userGroup->id] ?? [], + ]; + // if the user has a specific role in this group, then collect the memberships. + + return $return; + } +} diff --git a/app/User.php b/app/User.php index b8c27701e3..afb63fcf9d 100644 --- a/app/User.php +++ b/app/User.php @@ -26,6 +26,7 @@ namespace FireflyIII; use Eloquent; use Exception; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; @@ -48,6 +49,7 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\UserGroup; +use FireflyIII\Models\UserRole; use FireflyIII\Models\Webhook; use FireflyIII\Notifications\Admin\TestNotification; use FireflyIII\Notifications\Admin\UserInvitation; @@ -365,6 +367,91 @@ class User extends Authenticatable return 'objectguid'; } + + /** + * Does the user have role X in group Y? + * + * If $allowOverride is set to true, then the roles FULL or OWNER will also be checked, + * which means that in most cases the user DOES have access, regardless of the original role submitted in $role. + * + * @param UserGroup $userGroup + * @param UserRoleEnum $role + * @param bool $allowOverride + * + * @return bool + */ + public function hasRoleInGroup(UserGroup $userGroup, UserRoleEnum $role, bool $allowGroupOverride = false, bool $allowSystemOverride = false): bool + { + if ($allowSystemOverride && $this->hasRole('owner')) { + app('log')->debug(sprintf('hasRoleInGroup: user "#%d %s" is system owner and allowSystemOverride = true, return true', $this->id, $this->email)); + return true; + } + $roles = [$role->value]; + if ($allowGroupOverride) { + $roles[] = UserRoleEnum::OWNER->value; + $roles[] = UserRoleEnum::FULL->value; + } + app('log')->debug(sprintf('in hasRoleInGroup(%s)', join(', ', $roles))); + /** @var Collection $dbRoles */ + $dbRoles = UserRole::whereIn('title', $roles)->get(); + if (0 === $dbRoles->count()) { + app('log')->error(sprintf('Could not find role(s): %s. Probably migration mishap.', join(', ', $roles))); + return false; + } + $dbRolesIds = $dbRoles->pluck('id')->toArray(); + $dbRolesTitles = $dbRoles->pluck('title')->toArray(); + + /** @var Collection $groupMemberships */ + $groupMemberships = $this->groupMemberships() + ->whereIn('user_role_id', $dbRolesIds) + ->where('user_group_id', $userGroup->id)->get(); + if (0 === $groupMemberships->count()) { + app('log')->error(sprintf('User #%d "%s" does not have roles %s in user group #%d "%s"', + $this->id, $this->email, + join(', ', $roles), $userGroup->id, $userGroup->title)); + return false; + } + foreach ($groupMemberships as $membership) { + app('log')->debug(sprintf('User #%d "%s" has role "%s" in user group #%d "%s"', + $this->id, $this->email, + $membership->userRole->title, $userGroup->id, $userGroup->title)); + if (in_array($membership->userRole->title, $dbRolesTitles)) { + app('log')->debug(sprintf('Return true, found role "%s"', $membership->userRole->title)); + return true; + } + } + app('log')->error(sprintf('User #%d "%s" does not have roles %s in user group #%d "%s"', + $this->id, $this->email, + join(', ', $roles), $userGroup->id, $userGroup->title)); + return false; +// // not necessary, should always return true: +// $result = $groupMembership->userRole->title === $role->value; +// app('log')->error(sprintf('Does user #%d "%s" have role "%s" in user group #%d "%s"? %s', +// $this->id, $this->email, +// $role->value, $userGroup->id, $userGroup->title, var_export($result, true))); +// return $result; + } + + /** + * @param string $role + * + * @return bool + */ + public function hasRole(string $role): bool + { + return $this->roles()->where('name', $role)->count() === 1; + } + + /** + * Link to roles. + * + * @return BelongsToMany + */ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } + /** * * @return HasMany @@ -445,26 +532,6 @@ class User extends Authenticatable }; } - /** - * @param string $role - * - * @return bool - */ - public function hasRole(string $role): bool - { - return $this->roles()->where('name', $role)->count() === 1; - } - - /** - * Link to roles. - * - * @return BelongsToMany - */ - public function roles(): BelongsToMany - { - return $this->belongsToMany(Role::class); - } - /** * Route notifications for the Slack channel. * @@ -513,6 +580,8 @@ class User extends Authenticatable return $this->hasMany(Rule::class); } + // start LDAP related code + /** * Send the password reset notification. * @@ -525,8 +594,6 @@ class User extends Authenticatable event(new RequestedNewPassword($this, $token, $ipAddress)); } - // start LDAP related code - /** * Set the models LDAP domain. * diff --git a/app/Validation/Administration/ValidatesAdministrationAccess.php b/app/Validation/Administration/ValidatesAdministrationAccess.php index a03d35c248..f75aad5af7 100644 --- a/app/Validation/Administration/ValidatesAdministrationAccess.php +++ b/app/Validation/Administration/ValidatesAdministrationAccess.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Validation\Administration; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\UserRole; use FireflyIII\Repositories\User\UserRepositoryInterface; @@ -48,6 +49,7 @@ trait ValidatesAdministrationAccess */ protected function validateAdministration(Validator $validator, array $allowedRoles): void { + die('deprecated method, must be done through user.'); Log::debug('Now in validateAdministration()'); if (!auth()->check()) { Log::error('User is not authenticated.'); @@ -74,11 +76,11 @@ trait ValidatesAdministrationAccess $validator->errors()->add('administration', (string)trans('validation.no_access_user_group')); return; } - if (in_array(UserRole::OWNER, $array, true)) { + if (in_array(UserRoleEnum::OWNER->value, $array, true)) { Log::debug('User is owner of this administration.'); return; } - if (in_array(UserRole::FULL, $array, true)) { + if (in_array(UserRoleEnum::OWNER->value, $array, true)) { Log::debug('User has full access to this administration.'); return; } diff --git a/composer.lock b/composer.lock index bbe4f0fafd..df7fdee25f 100644 --- a/composer.lock +++ b/composer.lock @@ -4384,16 +4384,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.21", + "version": "3.0.23", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1" + "reference": "866cc78fbd82462ffd880e3f65692afe928bed50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/4580645d3fc05c189024eb3b834c6c1e4f0f30a1", - "reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/866cc78fbd82462ffd880e3f65692afe928bed50", + "reference": "866cc78fbd82462ffd880e3f65692afe928bed50", "shasum": "" }, "require": { @@ -4474,7 +4474,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.21" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.23" }, "funding": [ { @@ -4490,7 +4490,7 @@ "type": "tidelift" } ], - "time": "2023-07-09T15:24:48+00:00" + "time": "2023-09-18T17:22:01+00:00" }, { "name": "pragmarx/google2fa", @@ -9919,16 +9919,16 @@ }, { "name": "phpmyadmin/sql-parser", - "version": "5.8.0", + "version": "5.8.1", "source": { "type": "git", "url": "https://github.com/phpmyadmin/sql-parser.git", - "reference": "db1b3069b5dbc220d393d67ff911e0ae76732755" + "reference": "b877ee6262a00f0f498da5e01335e8a5dc01d203" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/db1b3069b5dbc220d393d67ff911e0ae76732755", - "reference": "db1b3069b5dbc220d393d67ff911e0ae76732755", + "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/b877ee6262a00f0f498da5e01335e8a5dc01d203", + "reference": "b877ee6262a00f0f498da5e01335e8a5dc01d203", "shasum": "" }, "require": { @@ -9950,7 +9950,7 @@ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", "psalm/plugin-phpunit": "^0.16.1", "vimeo/psalm": "^4.11", - "zumba/json-serializer": "^3.0" + "zumba/json-serializer": "~3.0.2" }, "suggest": { "ext-mbstring": "For best performance", @@ -10002,20 +10002,20 @@ "type": "other" } ], - "time": "2023-06-05T18:19:38+00:00" + "time": "2023-09-15T18:21:22+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.0", + "version": "1.24.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "3510b0a6274cc42f7219367cb3abfc123ffa09d6" + "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/3510b0a6274cc42f7219367cb3abfc123ffa09d6", - "reference": "3510b0a6274cc42f7219367cb3abfc123ffa09d6", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01", + "reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01", "shasum": "" }, "require": { @@ -10047,9 +10047,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.1" }, - "time": "2023-09-07T20:46:32+00:00" + "time": "2023-09-18T12:18:02+00:00" }, { "name": "phpstan/phpstan", @@ -10212,16 +10212,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.5", + "version": "10.1.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "1df504e42a88044c27a90136910f0b3fe9e91939" + "reference": "56f33548fe522c8d82da7ff3824b42829d324364" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1df504e42a88044c27a90136910f0b3fe9e91939", - "reference": "1df504e42a88044c27a90136910f0b3fe9e91939", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/56f33548fe522c8d82da7ff3824b42829d324364", + "reference": "56f33548fe522c8d82da7ff3824b42829d324364", "shasum": "" }, "require": { @@ -10278,7 +10278,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.5" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.6" }, "funding": [ { @@ -10286,7 +10286,7 @@ "type": "github" } ], - "time": "2023-09-12T14:37:22+00:00" + "time": "2023-09-19T04:59:03+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10533,16 +10533,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.3.4", + "version": "10.3.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b8d59476f19115c9774b3b447f78131781c6c32b" + "reference": "747c3b2038f1139e3dcd9886a3f5a948648b7503" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b8d59476f19115c9774b3b447f78131781c6c32b", - "reference": "b8d59476f19115c9774b3b447f78131781c6c32b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/747c3b2038f1139e3dcd9886a3f5a948648b7503", + "reference": "747c3b2038f1139e3dcd9886a3f5a948648b7503", "shasum": "" }, "require": { @@ -10566,7 +10566,7 @@ "sebastian/comparator": "^5.0", "sebastian/diff": "^5.0", "sebastian/environment": "^6.0", - "sebastian/exporter": "^5.0", + "sebastian/exporter": "^5.1", "sebastian/global-state": "^6.0.1", "sebastian/object-enumerator": "^5.0", "sebastian/recursion-context": "^5.0", @@ -10614,7 +10614,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.3.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.3.5" }, "funding": [ { @@ -10630,7 +10630,7 @@ "type": "tidelift" } ], - "time": "2023-09-12T14:42:28+00:00" + "time": "2023-09-19T05:42:37+00:00" }, { "name": "sebastian/cli-parser", @@ -11067,16 +11067,16 @@ }, { "name": "sebastian/exporter", - "version": "5.0.1", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "32ff03d078fed1279c4ec9a407d08c5e9febb480" + "reference": "c3fa8483f9539b190f7cd4bfc4a07631dd1df344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/32ff03d078fed1279c4ec9a407d08c5e9febb480", - "reference": "32ff03d078fed1279c4ec9a407d08c5e9febb480", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c3fa8483f9539b190f7cd4bfc4a07631dd1df344", + "reference": "c3fa8483f9539b190f7cd4bfc4a07631dd1df344", "shasum": "" }, "require": { @@ -11133,7 +11133,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.0" }, "funding": [ { @@ -11141,7 +11141,7 @@ "type": "github" } ], - "time": "2023-09-08T04:46:58+00:00" + "time": "2023-09-18T07:15:37+00:00" }, { "name": "sebastian/global-state", diff --git a/config/firefly.php b/config/firefly.php index f440871842..5291ed5c5b 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -46,6 +46,7 @@ use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournalLink; use FireflyIII\Models\TransactionType as TransactionTypeModel; +use FireflyIII\Models\UserGroup; use FireflyIII\Models\Webhook; use FireflyIII\Models\WebhookAttempt; use FireflyIII\Models\WebhookMessage; @@ -482,6 +483,7 @@ return [ // V2 API endpoints: 'userGroupAccount' => UserGroupAccount::class, 'userGroupBill' => UserGroupBill::class, + 'userGroup' => UserGroup::class, ], diff --git a/config/user_roles.php b/config/user_roles.php index 3eb1092dd8..e7c057a2b4 100644 --- a/config/user_roles.php +++ b/config/user_roles.php @@ -23,18 +23,12 @@ declare(strict_types=1); -use FireflyIII\Models\UserRole; +use FireflyIII\Enums\UserRoleEnum; -return [ +$result = []; - 'roles' => [ - UserRole::READ_ONLY => [], - UserRole::CHANGE_TRANSACTIONS => [], - UserRole::CHANGE_RULES => [], - UserRole::CHANGE_PIGGY_BANKS => [], - UserRole::CHANGE_REPETITIONS => [], - UserRole::VIEW_REPORTS => [], - UserRole::FULL => [], - UserRole::OWNER => [], - ], -]; + +foreach (UserRoleEnum::cases() as $role) { + $result[$role->value] = []; +} +return $result; diff --git a/database/seeders/UserRoleSeeder.php b/database/seeders/UserRoleSeeder.php index 7fcb812233..2eb8dc6059 100644 --- a/database/seeders/UserRoleSeeder.php +++ b/database/seeders/UserRoleSeeder.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace Database\Seeders; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\UserRole; use Illuminate\Database\Seeder; use PDOException; @@ -40,18 +41,10 @@ class UserRoleSeeder extends Seeder */ public function run() { - $roles = [ - UserRole::READ_ONLY, - UserRole::CHANGE_TRANSACTIONS, - UserRole::CHANGE_RULES, - UserRole::CHANGE_PIGGY_BANKS, - UserRole::CHANGE_REPETITIONS, - UserRole::VIEW_REPORTS, - UserRole::MANAGE_WEBHOOKS, - UserRole::MANAGE_CURRENCIES, - UserRole::FULL, - UserRole::OWNER, - ]; + $roles = []; + foreach (UserRoleEnum::cases() as $role) { + $roles[] = $role->value; + } /** @var string $role */ foreach ($roles as $role) {