diff --git a/app/Exceptions/Common/TooManyEmailsSent.php b/app/Exceptions/Common/TooManyEmailsSent.php new file mode 100644 index 000000000..2831835f5 --- /dev/null +++ b/app/Exceptions/Common/TooManyEmailsSent.php @@ -0,0 +1,10 @@ +middleware('permission:delete-sales-invoices')->only('destroy'); } - /** - * Show the form for creating a new resource. - * - * @param Document $invoice - * - * @return Response - */ - public function create(Document $invoice) + public function create(Document $invoice): JsonResponse { $notification = new Notification($invoice, 'invoice_new_customer', true); @@ -58,16 +55,9 @@ class InvoiceEmails extends Controller ]); } - /** - * Store a newly created resource in storage. - * - * @param Request $request - * - * @return Response - */ - public function store(Request $request) + public function store(Request $request): JsonResponse { - $response = $this->ajaxDispatch(new SendDocumentAsCustomMail($request, 'invoice_new_customer')); + $response = $this->sendEmail(new SendDocumentAsCustomMail($request, 'invoice_new_customer')); if ($response['success']) { $invoice = Document::find($request->get('document_id')); diff --git a/app/Http/Controllers/Modals/TransactionEmails.php b/app/Http/Controllers/Modals/TransactionEmails.php index b40eb6e3c..e4672906b 100644 --- a/app/Http/Controllers/Modals/TransactionEmails.php +++ b/app/Http/Controllers/Modals/TransactionEmails.php @@ -8,9 +8,13 @@ use App\Notifications\Portal\PaymentReceived as PaymentReceivedNotification; use App\Notifications\Banking\Transaction as TransactionNotification; use App\Jobs\Banking\SendTransactionAsCustomMail; use App\Http\Requests\Common\CustomMail as Request; +use Illuminate\Http\JsonResponse; +use App\Traits\Emails; class TransactionEmails extends Controller { + use Emails; + /** * Instantiate a new controller instance. */ @@ -23,17 +27,10 @@ class TransactionEmails extends Controller $this->middleware('permission:delete-banking-transactions')->only('destroy'); } - /** - * Show the form for creating a new resource. - * - * @param Transaction $transaction - * - * @return Response - */ - public function create(Transaction $transaction) - { + public function create(Transaction $transaction): JsonResponse + { $email_template = config('type.transaction.' . $transaction->type . '.email_template'); - + if (request()->get('email_template')) { $email_template = request()->get('email_template'); } @@ -42,7 +39,7 @@ class TransactionEmails extends Controller case 'invoice_payment_customer': $notification = new PaymentReceivedNotification($transaction->document, $transaction, $email_template, true); break; - + default: $notification = new TransactionNotification($transaction, $email_template, true); break; @@ -73,20 +70,13 @@ class TransactionEmails extends Controller ]); } - /** - * Store a newly created resource in storage. - * - * @param Request $request - * - * @return Response - */ - public function store(Request $request) + public function store(Request $request): JsonResponse { $transaction = Transaction::find($request->get('transaction_id')); $email_template = config('type.transaction.' . $transaction->type . '.email_template'); - $response = $this->ajaxDispatch(new SendTransactionAsCustomMail($request, $email_template)); + $response = $this->sendEmail(new SendTransactionAsCustomMail($request, $email_template)); if ($response['success']) { $route = config('type.transaction.' . $transaction->type . '.route.prefix'); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f7cc49ecc..8d320b7a7 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -134,6 +134,10 @@ class Kernel extends HttpKernel 'import' => [ 'throttle:import', ], + + 'email' => [ + 'throttle:email', + ], ]; /** diff --git a/app/Providers/Route.php b/app/Providers/Route.php index 952966034..69034ef4c 100644 --- a/app/Providers/Route.php +++ b/app/Providers/Route.php @@ -289,5 +289,12 @@ class Route extends Provider RateLimiter::for('import', function (Request $request) { return Limit::perMinute(config('app.throttles.import')); }); + + RateLimiter::for('email', function (Request $request) { + return [ + Limit::perDay(config('app.throttles.email.month'), 30), + Limit::perMinute(config('app.throttles.email.minute')), + ]; + }); } } diff --git a/app/Traits/Emails.php b/app/Traits/Emails.php new file mode 100644 index 000000000..ce8388a80 --- /dev/null +++ b/app/Traits/Emails.php @@ -0,0 +1,55 @@ +id; + $limit_per_month = config('app.throttles.email.month'); + $decay_per_month = 60 * 60 * 24 * 30; + + $can_send = RateLimiter::attempt($key_per_month, $limit_per_month, fn() => '', $decay_per_month); + + if ($can_send) { + // Check if the user has reached the limit of emails per minute + $key_per_minute = 'email-minute:' . user()->id; + $limit_per_minute = config('app.throttles.email.minute'); + + $can_send = RateLimiter::attempt($key_per_minute, $limit_per_minute, fn() => ''); + } + + if ($can_send) { + $this->dispatch($job); + + $response = [ + 'success' => true, + 'error' => false, + 'data' => '', + 'message' => '', + ]; + + return $response; + } + + $response = [ + 'success' => false, + 'error' => true, + 'data' => null, + 'message' => 'Too many emails sent!', + ]; + + report(new TooManyEmailsSent('Too many emails sent!')); + + return $response; + } +} diff --git a/config/app.php b/config/app.php index dbf827b95..a01930e41 100644 --- a/config/app.php +++ b/config/app.php @@ -25,6 +25,10 @@ return [ 'throttles' => [ 'api' => env('APP_THROTTLES_API', '60'), 'import' => env('APP_THROTTLES_IMPORT', '1'), + 'email' => [ + 'minute' => env('APP_THROTTLES_EMAIL_MINUTE', '3'), + 'month' => env('APP_THROTTLES_EMAIL_MONTH', '500'), + ], ], /* diff --git a/resources/views/components/layouts/admin/menu.blade.php b/resources/views/components/layouts/admin/menu.blade.php index e63cb67e6..208aaf334 100644 --- a/resources/views/components/layouts/admin/menu.blade.php +++ b/resources/views/components/layouts/admin/menu.blade.php @@ -19,7 +19,7 @@ - {{ trans('general.title.new', ['type' => trans_choice('general.expenses', 1)]) }} + {{ trans('general.title.new', ['type' => trans_choice('general.expenses', 1)]) }} @endcan @@ -36,7 +36,7 @@ data-menu="profile-menu" > @if (setting('default.use_gravatar', '0') == '1') diff --git a/resources/views/components/layouts/admin/scripts.blade.php b/resources/views/components/layouts/admin/scripts.blade.php index b75deebbf..b846eceab 100644 --- a/resources/views/components/layouts/admin/scripts.blade.php +++ b/resources/views/components/layouts/admin/scripts.blade.php @@ -139,6 +139,9 @@ if (button.getAttribute("data-menu") !== menuRef && iconButton.children[0].textContent != "cancel") { button.children[0].textContent = button.children[0].getAttribute("name"); button.children[0].classList.remove("active"); // inactive icon + + let split_id = button.children[0].id.split("-cancel"); + button.children[0].id = split_id[0]; } } }); @@ -148,6 +151,7 @@ if (menu.classList.contains(menuRef) && iconButton.children[0].textContent != "cancel") { iconButton.children[0].textContent = "cancel"; iconButton.children[0].classList.add("active"); + iconButton.children[0].id += "-cancel"; menu.classList.remove("ltr:-left-80", "rtl:-right-80"); menu.classList.add("ltr:left-14", "rtl:right-14"); @@ -177,6 +181,9 @@ iconButton.children[0].textContent = icon; iconButton.children[0].classList.remove("active"); + let split_id = iconButton.children[0].id.split("-cancel"); + iconButton.children[0].id = split_id[0]; + menu.classList.add("ltr:-left-80", "rtl:-right-80"); menu.classList.remove("ltr:left-14", "rtl:right-14"); @@ -242,6 +249,7 @@ settings_icon_html.children[0].textContent = "cancel"; settings_icon_html.children[0].classList.add("active"); + settings_icon_html.children[0].id += "-cancel"; toggleButton.classList.add("invisible"); } diff --git a/resources/views/components/layouts/portal/head.blade.php b/resources/views/components/layouts/portal/head.blade.php index de4c696cd..52bae9384 100644 --- a/resources/views/components/layouts/portal/head.blade.php +++ b/resources/views/components/layouts/portal/head.blade.php @@ -26,6 +26,7 @@ + diff --git a/resources/views/components/layouts/portal/scripts.blade.php b/resources/views/components/layouts/portal/scripts.blade.php index 4a82d6b95..e5c4b94f8 100644 --- a/resources/views/components/layouts/portal/scripts.blade.php +++ b/resources/views/components/layouts/portal/scripts.blade.php @@ -122,6 +122,9 @@ if (button.getAttribute("data-menu") !== menuRef && iconButton.children[0].textContent != "cancel") { button.children[0].textContent = button.children[0].getAttribute("name"); button.children[0].classList.remove("active"); // inactive icon + + let split_id = button.children[0].id.split("-cancel"); + button.children[0].id = split_id[0]; } } }); @@ -131,6 +134,7 @@ if (menu.classList.contains(menuRef) && iconButton.children[0].textContent != "cancel") { iconButton.children[0].textContent = "cancel"; iconButton.children[0].classList.add("active"); + iconButton.children[0].id += "-cancel"; menu.classList.remove("ltr:-left-80", "rtl:-right-80"); menu.classList.add("ltr:left-14", "rtl:right-14"); @@ -155,6 +159,9 @@ iconButton.children[0].textContent = icon; iconButton.children[0].classList.remove("active"); + let split_id = iconButton.children[0].id.split("-cancel"); + iconButton.children[0].id = split_id[0]; + menu.classList.add("ltr:-left-80", "rtl:-right-80"); menu.classList.remove("ltr:left-14", "rtl:right-14"); mainContent.classList.remove("hidden"); diff --git a/resources/views/components/layouts/signed/head.blade.php b/resources/views/components/layouts/signed/head.blade.php index de4c696cd..52bae9384 100644 --- a/resources/views/components/layouts/signed/head.blade.php +++ b/resources/views/components/layouts/signed/head.blade.php @@ -26,6 +26,7 @@ + diff --git a/resources/views/portal/invoices/show.blade.php b/resources/views/portal/invoices/show.blade.php index 239a04d13..67844fe81 100644 --- a/resources/views/portal/invoices/show.blade.php +++ b/resources/views/portal/invoices/show.blade.php @@ -33,7 +33,7 @@
{{ $name }} diff --git a/routes/admin.php b/routes/admin.php index 2ad6b90bf..f81e103e1 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -259,9 +259,13 @@ Route::group(['as' => 'modals.', 'prefix' => 'modals'], function () { 'middleware' => ['date.format', 'money', 'dropzone'] ]); - Route::resource('transactions/{transaction}/emails', 'Modals\TransactionEmails', ['names' => 'transactions.emails']); - Route::resource('transactions/{transaction}/share', 'Modals\TransactionShare', ['names' => 'transactions.share']); - Route::resource('invoices/{invoice}/emails', 'Modals\InvoiceEmails', ['names' => 'invoices.emails']); - Route::resource('invoices/{invoice}/share', 'Modals\InvoiceShare', ['names' => 'invoices.share']); + Route::get('invoices/{invoice}/emails/create', 'Modals\InvoiceEmails@create')->name('invoices.emails.create'); + Route::post('invoices/{invoice}/emails', 'Modals\InvoiceEmails@store')->name('invoices.emails.store'); + Route::get('invoices/{invoice}/share/create', 'Modals\InvoiceShare@create')->name('invoices.share.create'); + + Route::get('transactions/{transaction}/emails/create', 'Modals\TransactionEmails@create')->name('transactions.emails.create'); + Route::post('transactions/{transaction}/emails', 'Modals\TransactionEmails@store')->name('transactions.emails.store'); + Route::get('transactions/{transaction}/share/create', 'Modals\TransactionShare@create')->name('transactions.share.create'); + Route::resource('taxes', 'Modals\Taxes'); });