diff --git a/app/Api/V1/Controllers/Data/PurgeController.php b/app/Api/V1/Controllers/Data/PurgeController.php new file mode 100644 index 0000000000..34fa6d8695 --- /dev/null +++ b/app/Api/V1/Controllers/Data/PurgeController.php @@ -0,0 +1,93 @@ +. + */ + +namespace FireflyIII\Api\V1\Controllers\Data; + +use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Models\Account; +use FireflyIII\Models\Bill; +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; +use FireflyIII\Models\ObjectGroup; +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\Recurrence; +use FireflyIII\Models\Rule; +use FireflyIII\Models\RuleGroup; +use FireflyIII\Models\Tag; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Http\JsonResponse; + +class PurgeController extends Controller +{ + /** + * @return JsonResponse + */ + public function purge(): JsonResponse + { + + $user = auth()->user(); + + // some manual code, too lazy to call all repositories. + //,transactions,withdrawals,deposits,transfers'; + + // budgets: + Budget::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + // bills + Bill::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + // piggies + $set = PiggyBank::leftJoin('accounts','accounts.id','piggy_banks.account_id') + ->where('accounts.user_id', $user->id)->onlyTrashed()->get(['piggy_banks.*']); + /** @var PiggyBank $piggy */ + foreach($set as $piggy) { + $piggy->forceDelete(); + } + + // rule group + RuleGroup::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + // rules + Rule::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + // recurring transactions + Recurrence::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + // categories + Category::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + // tags + Tag::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + + // accounts + Account::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + // transaction groups + TransactionGroup::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + // transaction journals + TransactionJournal::whereUserId($user->id)->onlyTrashed()->forceDelete(); + + return response()->json([], 204); + } +} diff --git a/app/Events/Admin/InvitationCreated.php b/app/Events/Admin/InvitationCreated.php index a91fc89e4c..ac9d860255 100644 --- a/app/Events/Admin/InvitationCreated.php +++ b/app/Events/Admin/InvitationCreated.php @@ -1,4 +1,5 @@ invitee->email; + $admin = $event->invitee->user->email; + $url = route('invite', [$event->invitee->invite_code]); + try { + Mail::to($invitee)->send(new InvitationMail($invitee, $admin, $url)); + + } catch (Exception $e) { // @phpstan-ignore-line + Log::error($e->getMessage()); + } + } + /** * @param RegisteredUser $event * @return bool diff --git a/app/Helpers/Collector/Extensions/MetaCollection.php b/app/Helpers/Collector/Extensions/MetaCollection.php index 4f94df0d79..d2347db148 100644 --- a/app/Helpers/Collector/Extensions/MetaCollection.php +++ b/app/Helpers/Collector/Extensions/MetaCollection.php @@ -237,9 +237,6 @@ trait MetaCollection return $this; } - - - /** * @param string $url * @return GroupCollectorInterface @@ -269,9 +266,6 @@ trait MetaCollection return $this; } - - - /** * @param string $url * @return GroupCollectorInterface diff --git a/app/Helpers/Collector/GroupCollectorInterface.php b/app/Helpers/Collector/GroupCollectorInterface.php index f0cba4abf9..26e0fb2d74 100644 --- a/app/Helpers/Collector/GroupCollectorInterface.php +++ b/app/Helpers/Collector/GroupCollectorInterface.php @@ -411,9 +411,6 @@ interface GroupCollectorInterface * @return GroupCollectorInterface */ public function externalIdDoesNotContain(string $externalId): GroupCollectorInterface; - - - /** * @param string $externalId * @return GroupCollectorInterface @@ -437,9 +434,6 @@ interface GroupCollectorInterface * @return GroupCollectorInterface */ public function externalIdDoesNotStart(string $externalId): GroupCollectorInterface; - - - /** * @param string $url * @return GroupCollectorInterface @@ -1038,9 +1032,6 @@ interface GroupCollectorInterface * @return GroupCollectorInterface */ public function excludeInternalReference(string $externalId): GroupCollectorInterface; - - - /** * Limit the result to a set of specific transaction journals. * diff --git a/app/Http/Controllers/Budget/BudgetLimitController.php b/app/Http/Controllers/Budget/BudgetLimitController.php index 34c1de175c..200d7d77b0 100644 --- a/app/Http/Controllers/Budget/BudgetLimitController.php +++ b/app/Http/Controllers/Budget/BudgetLimitController.php @@ -151,10 +151,14 @@ class BudgetLimitController extends Controller // sanity check on amount: if ((float) $amount === 0.0) { - $amount = '1'; + if (null !== $limit) { + $this->blRepository->destroyBudgetLimit($limit); + } + // return empty=ish array: + return response()->json([]); } - if ((int) $amount > 65536) { - $amount = '65536'; + if ((int) $amount > 16777216) { + $amount = '16777216'; } if (null !== $limit) { @@ -175,7 +179,7 @@ class BudgetLimitController extends Controller if ($request->expectsJson()) { $array = $limit->toArray(); - // add some extra meta data: + // add some extra metadata: $spentArr = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection([$budget]), $currency); $array['spent'] = $spentArr[$currency->id]['sum'] ?? '0'; $array['left_formatted'] = app('amount')->formatAnything($limit->transactionCurrency, bcadd($array['spent'], $array['amount'])); @@ -208,10 +212,19 @@ class BudgetLimitController extends Controller // sanity check on amount: if ((float) $amount === 0.0) { - $amount = '1'; + $budgetId = $budgetLimit->budget_id; + $currency = $budgetLimit->transactionCurrency; + $this->blRepository->destroyBudgetLimit($budgetLimit); + $array = [ + 'budget_id' => $budgetId, + 'left_formatted' => app('amount')->formatAnything($currency, '0'), + 'left_per_day_formatted' => app('amount')->formatAnything($currency, '0'), + 'transaction_currency_id' => $currency->id, + ]; + return response()->json($array); } - if ((int) $amount > 65536) { - $amount = '65536'; + if ((int) $amount > 16777216) { // 16 million + $amount = '16777216'; } $limit = $this->blRepository->update($budgetLimit, ['amount' => $amount]); diff --git a/app/Http/Requests/InviteUserFormRequest.php b/app/Http/Requests/InviteUserFormRequest.php index fb603b74ee..628b655c29 100644 --- a/app/Http/Requests/InviteUserFormRequest.php +++ b/app/Http/Requests/InviteUserFormRequest.php @@ -1,4 +1,5 @@ float('foreign_amount')) { + if (null !== $this->convertFloat('foreign_amount')) { $return['transactions'][0]['foreign_amount'] = $this->convertString('foreign_amount'); $return['transactions'][0]['foreign_currency_id'] = $this->convertInteger('foreign_currency_id'); } @@ -228,7 +228,7 @@ class RecurrenceFormRequest extends FormRequest $rules['repetitions'] = 'required|numeric|between:0,254'; } // if foreign amount, currency must be different. - if (null !== $this->float('foreign_amount')) { + if (null !== $this->convertFloat('foreign_amount')) { $rules['foreign_currency_id'] = 'exists:transaction_currencies,id|different:transaction_currency_id'; } diff --git a/app/Mail/InvitationMail.php b/app/Mail/InvitationMail.php new file mode 100644 index 0000000000..5ad5ee1ac9 --- /dev/null +++ b/app/Mail/InvitationMail.php @@ -0,0 +1,61 @@ +. + */ + +namespace FireflyIII\Mail; + +use Illuminate\Bus\Queueable; +use Illuminate\Mail\Mailable; +use Illuminate\Queue\SerializesModels; + +class InvitationMail extends Mailable +{ + use Queueable, SerializesModels; + + public string $invitee; + public string $admin; + public string $url; + public string $host; + + /** + * OAuthTokenCreatedMail constructor. + * + * @param string $ipAddress + */ + public function __construct(string $invitee, string $admin, string $url) + { + $this->invitee = $invitee; + $this->admin = $admin; + $this->url = $url; + $this->host = parse_url($url, PHP_URL_HOST); + } + + /** + * Build the message. + * + * @return $this + */ + public function build(): self + { + return $this + ->markdown('emails.invitation') + ->subject((string) trans('email.invite_user_subject')); + } +} diff --git a/app/Models/InvitedUser.php b/app/Models/InvitedUser.php index 22e0c7bf15..09f7c4ad25 100644 --- a/app/Models/InvitedUser.php +++ b/app/Models/InvitedUser.php @@ -1,4 +1,5 @@ [ 'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification', - //'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite', + 'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite', ], // is a Transaction Journal related event. diff --git a/app/Repositories/User/UserRepository.php b/app/Repositories/User/UserRepository.php index 4d6c60dc95..3770c1b550 100644 --- a/app/Repositories/User/UserRepository.php +++ b/app/Repositories/User/UserRepository.php @@ -451,7 +451,7 @@ class UserRepository implements UserRepositoryInterface public function validateInviteCode(string $code): bool { $now = Carbon::now(); - $invitee = InvitedUser::where('invite_code', $code)->where('expires', '<=', $now)->where('redeemed', 0)->first(); + $invitee = InvitedUser::where('invite_code', $code)->where('expires', '>', $now->format('Y-m-d H:i:s'))->where('redeemed', 0)->first(); return null !== $invitee; } diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php index f2c39bfaf1..05b06f92cd 100644 --- a/app/Support/Request/ConvertsDataTypes.php +++ b/app/Support/Request/ConvertsDataTypes.php @@ -219,7 +219,7 @@ trait ConvertsDataTypes * * @return float|null */ - protected function float(string $field): ?float + protected function convertFloat(string $field): ?float { $res = $this->get($field); if (null === $res) { diff --git a/app/TransactionRules/Actions/AppendDescriptionToNotes.php b/app/TransactionRules/Actions/AppendDescriptionToNotes.php index 7ce4e37a5d..783320c0ce 100644 --- a/app/TransactionRules/Actions/AppendDescriptionToNotes.php +++ b/app/TransactionRules/Actions/AppendDescriptionToNotes.php @@ -1,4 +1,5 @@ 0) { @@ -113,6 +114,7 @@ function updateBudgetedAmount(e) { amount: input.val(), }).done(function (data) { input.prop('disabled', false); + input.data('limit', data.id); $('.left_span[data-limit="' + budgetLimitId + '"]').html(data.left_formatted); if (data.left_per_day > 0) { $('.left_span[data-limit="' + budgetLimitId + '"]').html(data.left_formatted + '(' + data.left_per_day_formatted + ')'); diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php index a8d96e80b4..97d02af36f 100644 --- a/resources/lang/en_US/email.php +++ b/resources/lang/en_US/email.php @@ -36,6 +36,10 @@ return [ // invite 'invitation_created_subject' => 'An invitation has been created', 'invitation_created_body' => 'Admin user ":email" created a user invitation which can be used by whoever is behind email address ":invitee". The invite will be valid for 48hrs.', + 'invite_user_subject' => 'You\'ve been invited to create a Firefly III account.', + 'invitation_introduction' => 'You\'ve been invited to create a Firefly III account on **:host**. Firefly III is a personal, self-hosted, private personal finance manager. All the cool kids are using it.', + 'invitation_invited_by' => 'You\'ve been invited by ":admin" and this invitation was sent to ":invitee". That\'s you, right?', + 'invitation_url' => 'The invitation is valid for 48 hours and can be redeemed by surfing to [Firefly III](:url). Enjoy!', // new IP 'login_from_new_ip' => 'New login on Firefly III', diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index b3321b00c7..9698373633 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1336,8 +1336,14 @@ return [ 'slack_url_label' => 'Slack "incoming webhook" URL', // profile: - 'delete_stuff_header' => 'Delete data', - 'permanent_delete_stuff' => 'Be careful with these buttons. Deleting stuff is permanent.', + 'purge_data_title' => 'Purge data from Firefly III', + 'purge_data_expl' => '"Purging" means "deleting that which is already deleted". In normal circumstances, Firefly III deletes nothing permanently. It just hides it. This can be annoying when you import data from other sources, as removed transactions will still be recognized as possible duplicates. The button below deletes all of these previously "deleted" records FOREVER.', + 'delete_stuff_header' => 'Delete and purge data', + 'purge_all_data' => 'Purge all deleted records', + 'purge_data' => 'Purge data', + 'purged_all_records' => 'All deleted records have been purged.', + 'delete_data_title' => 'Delete data from Firefly III', + 'permanent_delete_stuff' => 'You can delete stuff from Firefly III. Using the buttons below means that your items will be removed from view and hidden. There is no undo-button for this, but the items may remain in the database where you can salvage them if necessary.', 'other_sessions_logged_out' => 'All your other sessions have been logged out.', 'delete_all_budgets' => 'Delete ALL your budgets', 'delete_all_categories' => 'Delete ALL your categories', @@ -2283,6 +2289,7 @@ return [ 'admin_notification_check_new_version' => 'A new version is available', 'admin_notification_check_invite_created' => 'A user is invited to Firefly III', 'admin_notification_check_invite_redeemed' => 'A user invitation is redeemed', + 'all_invited_users' => 'All invited users', 'save_notification_settings' => 'Save settings', 'notification_settings_saved' => 'The notification settings have been saved', diff --git a/resources/views/emails/invitation.blade.php b/resources/views/emails/invitation.blade.php new file mode 100644 index 0000000000..7f6cd99445 --- /dev/null +++ b/resources/views/emails/invitation.blade.php @@ -0,0 +1,8 @@ +@component('mail::message') +{{ trans('email.invitation_introduction', ['host' => $host]) }} + +{{ trans('email.invitation_invited_by', ['invitee' => $invitee, 'admin' => $admin]) }} + +{{ trans('email.invitation_url', ['url' => $url]) }} + +@endcomponent diff --git a/resources/views/profile/index.twig b/resources/views/profile/index.twig index f9e91e6fef..11a56516cd 100644 --- a/resources/views/profile/index.twig +++ b/resources/views/profile/index.twig @@ -13,7 +13,8 @@
{{ 'pref_two_factor_auth_help'|_ }}
- {% if enabled2FA == true %} -- {{ trans_choice('firefly.pref_two_factor_backup_code_count', mfaBackupCount) }} -
+ +{{ 'pref_two_factor_auth_help'|_ }}
+ {% if enabled2FA == true %} ++ {{ trans_choice('firefly.pref_two_factor_backup_code_count', mfaBackupCount) }} +
- - - - {% else %} - - {% endif %} + + + + {% else %} + + {% endif %} ++ {{ 'purge_data_expl'|_ }} +
+ + +
+ +{{ 'permanent_delete_stuff'|_ }}
+
+
+
{{ 'also_delete_transactions'|_ }}
-+
+ data-type="expense_accounts" class="confirm btn btn-warning btn-sm"> {{ 'delete_all_expense_accounts'|_ }} + + data-type="revenue_accounts" class="confirm btn btn-warning btn-sm"> {{ 'delete_all_revenue_accounts'|_ }} + -
++
+ data-type="deposits" class="confirm btn btn-warning btn-sm"> {{ 'delete_all_deposits'|_ }} + -
+