diff --git a/VERSION.txt b/VERSION.txt index bd96b42f4638..ed45e0a6e819 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.1.9 \ No newline at end of file +5.1.10 \ No newline at end of file diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 8c096364a68a..1c6bc259125d 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -652,13 +652,12 @@ class CompanySettings extends BaseSettings 'total_columns' => [ '$subtotal', '$discount', - '$total_taxes', - '$line_taxes', '$custom_surcharge1', '$custom_surcharge2', '$custom_surcharge3', '$custom_surcharge4', - '$total', + '$total_taxes', + '$line_taxes', '$paid_to_date', '$outstanding', ], diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 3bc064ea40a4..f53f47348ef8 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -13,6 +13,7 @@ namespace App\Http\Controllers; use App\Events\Credit\CreditWasEmailed; use App\Events\Quote\QuoteWasEmailed; +use App\Http\Middleware\UserVerified; use App\Http\Requests\Email\SendEmailRequest; use App\Jobs\Entity\EmailEntity; use App\Jobs\Mail\EntitySentMailer; @@ -130,7 +131,8 @@ class EmailController extends BaseController $entity_obj->service()->markSent()->save(); - EmailEntity::dispatch($invitation, $invitation->company, $template, $data)->delay(now()->addSeconds(5)); + EmailEntity::dispatch($invitation, $invitation->company, $template, $data) + ->delay(now()->addSeconds(5)); } diff --git a/app/Http/Controllers/PostMarkController.php b/app/Http/Controllers/PostMarkController.php index cb0ef144c219..e4f6a855d0af 100644 --- a/app/Http/Controllers/PostMarkController.php +++ b/app/Http/Controllers/PostMarkController.php @@ -78,10 +78,8 @@ class PostMarkController extends BaseController $this->invitation = $this->discoverInvitation($request->input('MessageID')); - if($this->invitation){ + if($this->invitation) $this->invitation->email_error = $request->input('Details'); - $this->invitation->save(); - } else return response()->json(['message' => 'Message not found']); @@ -122,6 +120,9 @@ class PostMarkController extends BaseController // } private function processDelivery($request) { + $this->invitation->email_status = 'delivered'; + $this->invitation->save(); + SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_DELIVERY, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client); } @@ -153,6 +154,9 @@ class PostMarkController extends BaseController private function processBounce($request) { + $this->invitation->email_status = 'bounced'; + $this->invitation->save(); + SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client); } @@ -183,6 +187,10 @@ class PostMarkController extends BaseController // } private function processSpamComplaint($request) { + + $this->invitation->email_status = 'spam'; + $this->invitation->save(); + SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index aa85deb5bcb6..87de725daaf8 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -386,6 +386,7 @@ class UserController extends BaseController /* When changing email address we store the former email in case we need to rollback */ if ($old_user_email != $new_email) { $user->last_confirmed_email_address = $old_user_email; + $user->email_verified_at = null; $user->save(); UserEmailChanged::dispatch($new_user, json_decode($old_user), auth()->user()->company()); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 892be0b99aea..63803aa46499 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -38,6 +38,7 @@ use App\Http\Middleware\TokenAuth; use App\Http\Middleware\TrimStrings; use App\Http\Middleware\TrustProxies; use App\Http\Middleware\UrlSetDb; +use App\Http\Middleware\UserVerified; use App\Http\Middleware\VerifyCsrfToken; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\Middleware\Authorize; @@ -157,5 +158,6 @@ class Kernel extends HttpKernel 'phantom_secret' => PhantomSecret::class, 'contact_key_login' => ContactKeyLogin::class, 'check_client_existence' => CheckClientExistence::class, + 'user_verified' => UserVerified::class, ]; } diff --git a/app/Http/Middleware/PasswordProtection.php b/app/Http/Middleware/PasswordProtection.php index c22c1181df1e..1df11b733400 100644 --- a/app/Http/Middleware/PasswordProtection.php +++ b/app/Http/Middleware/PasswordProtection.php @@ -53,6 +53,7 @@ class PasswordProtection /* Cannot allow duplicates! */ if ($existing_user = MultiDB::hasUser($query)) { + Cache::add(auth()->user()->email.'_logged_in', Str::random(64), now()->addMinutes(30)); return $next($request); } } diff --git a/app/Http/Middleware/UserVerified.php b/app/Http/Middleware/UserVerified.php new file mode 100644 index 000000000000..35a0acdeea81 --- /dev/null +++ b/app/Http/Middleware/UserVerified.php @@ -0,0 +1,52 @@ +user = $user ?: auth()->user(); + } + + /** + * Handle an incoming request. + * + * @param Request $request + * @param Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + + $error = [ + 'message' => 'Email confirmation required.', + 'errors' => new \stdClass, + ]; + + if ($this->user && !$this->user->isVerified()) + return response()->json($error, 403); + + return $next($request); + } +} diff --git a/app/Jobs/Account/CreateAccount.php b/app/Jobs/Account/CreateAccount.php index 882604331bab..f9131b004870 100644 --- a/app/Jobs/Account/CreateAccount.php +++ b/app/Jobs/Account/CreateAccount.php @@ -110,8 +110,7 @@ class CreateAccount NinjaMailerJob::dispatch($nmo); - - NinjaMailerJob::dispatchNow($nmo); + // NinjaMailerJob::dispatchNow($nmo); VersionCheck::dispatchNow(); diff --git a/app/Models/User.php b/app/Models/User.php index 36c7aec6ac39..52582d0a95ee 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -80,6 +80,7 @@ class User extends Authenticatable implements MustVerifyEmail 'custom_value3', 'custom_value4', 'is_deleted', + 'google_2fa_secret', ]; /** @@ -338,6 +339,11 @@ class User extends Authenticatable implements MustVerifyEmail return $this->morphMany(Document::class, 'documentable'); } + public function isVerified() + { + return is_null($this->email_verified_at) ? false : true; + } + public function getEmailVerifiedAt() { if ($this->email_verified_at) { diff --git a/app/Transformers/CreditInvitationTransformer.php b/app/Transformers/CreditInvitationTransformer.php index 4996c165c1a8..fca841563848 100644 --- a/app/Transformers/CreditInvitationTransformer.php +++ b/app/Transformers/CreditInvitationTransformer.php @@ -31,6 +31,8 @@ class CreditInvitationTransformer extends EntityTransformer 'updated_at' => (int) $invitation->updated_at, 'archived_at' => (int) $invitation->deleted_at, 'created_at' => (int) $invitation->created_at, + 'email_status' => $invitation->email_status, + 'email_error' => (string)$invitation->email_error, ]; } } diff --git a/app/Transformers/InvoiceInvitationTransformer.php b/app/Transformers/InvoiceInvitationTransformer.php index 4517bf35faa5..76a514c409a8 100644 --- a/app/Transformers/InvoiceInvitationTransformer.php +++ b/app/Transformers/InvoiceInvitationTransformer.php @@ -30,7 +30,9 @@ class InvoiceInvitationTransformer extends EntityTransformer 'opened_date' => $invitation->opened_date ?: '', 'updated_at' => (int) $invitation->updated_at, 'archived_at' => (int) $invitation->deleted_at, - 'created_at' => (int) $invitation->created_at, + 'created_at' => (int) $invitation->created_at, + 'email_status' => $invitation->email_status, + 'email_error' => (string)$invitation->email_error, ]; } } diff --git a/app/Transformers/QuoteInvitationTransformer.php b/app/Transformers/QuoteInvitationTransformer.php index 31eeb080de6b..021b3b74e380 100644 --- a/app/Transformers/QuoteInvitationTransformer.php +++ b/app/Transformers/QuoteInvitationTransformer.php @@ -31,6 +31,8 @@ class QuoteInvitationTransformer extends EntityTransformer 'updated_at' => (int) $invitation->updated_at, 'archived_at' => (int) $invitation->deleted_at, 'created_at' => (int) $invitation->created_at, + 'email_status' => $invitation->email_status, + 'email_error' => (string)$invitation->email_error, ]; } } diff --git a/app/Transformers/RecurringInvoiceInvitationTransformer.php b/app/Transformers/RecurringInvoiceInvitationTransformer.php index 815115030248..43f0682c56fb 100644 --- a/app/Transformers/RecurringInvoiceInvitationTransformer.php +++ b/app/Transformers/RecurringInvoiceInvitationTransformer.php @@ -31,6 +31,8 @@ class RecurringInvoiceInvitationTransformer extends EntityTransformer 'updated_at' => (int) $invitation->updated_at, 'archived_at' => (int) $invitation->deleted_at, 'created_at' => (int) $invitation->created_at, + 'email_status' => $invitation->email_status, + 'email_error' => (string)$invitation->email_error, ]; } } diff --git a/app/Transformers/UserTransformer.php b/app/Transformers/UserTransformer.php index dd6e2c82816e..bd3632ab69b7 100644 --- a/app/Transformers/UserTransformer.php +++ b/app/Transformers/UserTransformer.php @@ -59,6 +59,7 @@ class UserTransformer extends EntityTransformer 'custom_value4' => $user->custom_value4 ?: '', 'oauth_provider_id' => (string) $user->oauth_provider_id, 'last_confirmed_email_address' => (string) $user->last_confirmed_email_address ?: '', + 'google_2fa_secret' => (bool) $user->google_2fa_secret, ]; } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index f2fb53ce09b4..11dcbce10247 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -248,7 +248,6 @@ class HtmlEngine $data['$client.currency'] = ['value' => $this->client->currency()->code, 'label' => '']; $data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; - $data['$outstanding'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')]; diff --git a/config/ninja.php b/config/ninja.php index 74edc1485493..e2e6cf03b951 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -13,7 +13,7 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', ''), - 'app_version' => '5.1.9', + 'app_version' => '5.1.10', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), diff --git a/database/migrations/2021_02_25_205901_enum_invitations_email_status.php b/database/migrations/2021_02_25_205901_enum_invitations_email_status.php new file mode 100644 index 000000000000..cb7e8be1cce3 --- /dev/null +++ b/database/migrations/2021_02_25_205901_enum_invitations_email_status.php @@ -0,0 +1,42 @@ +enum('email_status', ['delivered', 'bounced', 'spam'])->nullable(); + }); + + Schema::table('quote_invitations', function(Blueprint $table){ + $table->enum('email_status', ['delivered', 'bounced', 'spam'])->nullable(); + }); + + Schema::table('credit_invitations', function(Blueprint $table){ + $table->enum('email_status', ['delivered', 'bounced', 'spam'])->nullable(); + }); + + Schema::table('recurring_invoice_invitations', function(Blueprint $table){ + $table->enum('email_status', ['delivered', 'bounced', 'spam'])->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/routes/api.php b/routes/api.php index 5ef05ac22167..5728607a20c1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -65,7 +65,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::get('documents/{document}/download', 'DocumentController@download')->name('documents.download'); Route::post('documents/bulk', 'DocumentController@bulk')->name('documents.bulk'); - Route::post('emails', 'EmailController@send')->name('email.send'); + Route::post('emails', 'EmailController@send')->name('email.send')->middleware('user_verified'); Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit Route::put('expenses/{expense}/upload', 'ExpenseController@upload');