diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php --- a/src/app/Backends/LDAP.php +++ b/src/app/Backends/LDAP.php @@ -757,8 +757,10 @@ */ private static function setUserAttributes(User $user, array &$entry) { - $firstName = $user->getSetting('first_name'); - $lastName = $user->getSetting('last_name'); + $settings = $user->getSettings(['first_name', 'last_name', 'organization']); + + $firstName = $settings['first_name']; + $lastName = $settings['last_name']; $cn = "unknown"; $displayname = ""; @@ -788,7 +790,7 @@ $entry['sn'] = $lastName; $entry['userpassword'] = $user->password_ldap; $entry['inetuserstatus'] = $user->status; - $entry['o'] = $user->getSetting('organization'); + $entry['o'] = $settings['organization']; $entry['mailquota'] = 0; $entry['alias'] = $user->aliases->pluck('alias')->toArray(); diff --git a/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php b/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php @@ -0,0 +1,14 @@ +getObject(\App\Tenant::class, $this->argument('tenant'), 'title'); + + if (!$tenant) { + $this->error("Unable to find the tenant."); + return 1; + } + + $tenant->settings()->orderBy('key')->get() + ->each(function ($entry) { + $text = "{$entry->key}: {$entry->value}"; + $this->info($text); + }); + } +} diff --git a/src/app/Documents/Receipt.php b/src/app/Documents/Receipt.php --- a/src/app/Documents/Receipt.php +++ b/src/app/Documents/Receipt.php @@ -229,10 +229,9 @@ { $user = $this->wallet->owner; $name = $user->name(); - $organization = $user->getSetting('organization'); - $address = $user->getSetting('billing_address'); + $settings = $user->getSettings(['organization', 'billing_address']); - $customer = trim(($organization ?: $name) . "\n$address"); + $customer = trim(($settings['organization'] ?: $name) . "\n" . $settings['billing_address']); $customer = str_replace("\n", '
', htmlentities($customer)); return [ diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php --- a/src/app/Http/Controllers/API/V4/OpenViduController.php +++ b/src/app/Http/Controllers/API/V4/OpenViduController.php @@ -245,11 +245,12 @@ } } - $password = (string) $room->getSetting('password'); + $settings = $room->getSettings(['locked', 'nomedia', 'password']); + $password = (string) $settings['password']; $config = [ - 'locked' => $room->getSetting('locked') === 'true', - 'nomedia' => $room->getSetting('nomedia') === 'true', + 'locked' => $settings['locked'] === 'true', + 'nomedia' => $settings['nomedia'] === 'true', 'password' => $isOwner ? $password : '', 'requires_password' => !$isOwner && strlen($password), ]; diff --git a/src/app/Http/Controllers/API/V4/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php --- a/src/app/Http/Controllers/API/V4/PaymentsController.php +++ b/src/app/Http/Controllers/API/V4/PaymentsController.php @@ -294,12 +294,14 @@ */ public static function topUpWallet(Wallet $wallet): bool { - if ((bool) $wallet->getSetting('mandate_disabled')) { + $settings = $wallet->getSettings(['mandate_disabled', 'mandate_balance', 'mandate_amount']); + + if (!empty($settings['mandate_disabled'])) { return false; } - $min_balance = (int) (floatval($wallet->getSetting('mandate_balance')) * 100); - $amount = (int) (floatval($wallet->getSetting('mandate_amount')) * 100); + $min_balance = (int) (floatval($settings['mandate_balance']) * 100); + $amount = (int) (floatval($settings['mandate_amount']) * 100); // The wallet balance is greater than the auto-payment threshold if ($wallet->balance >= $min_balance) { @@ -346,16 +348,17 @@ public static function walletMandate(Wallet $wallet): array { $provider = PaymentProvider::factory($wallet); + $settings = $wallet->getSettings(['mandate_disabled', 'mandate_balance', 'mandate_amount']); // Get the Mandate info $mandate = (array) $provider->getMandate($wallet); $mandate['amount'] = (int) (PaymentProvider::MIN_AMOUNT / 100); $mandate['balance'] = 0; - $mandate['isDisabled'] = !empty($mandate['id']) && $wallet->getSetting('mandate_disabled'); + $mandate['isDisabled'] = !empty($mandate['id']) && $settings['mandate_disabled']; foreach (['amount', 'balance'] as $key) { - if (($value = $wallet->getSetting("mandate_{$key}")) !== null) { + if (($value = $settings["mandate_{$key}"]) !== null) { $mandate[$key] = $value; } } diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php --- a/src/app/Http/Controllers/API/V4/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/WalletsController.php @@ -126,7 +126,7 @@ $wallet = Wallet::find($id); if (empty($wallet) || !$this->checkTenant($wallet->owner)) { - return $this->errorResponse(404); + abort(404); } // Only owner (or admin) has access to the wallet diff --git a/src/app/Jobs/PasswordResetEmail.php b/src/app/Jobs/PasswordResetEmail.php --- a/src/app/Jobs/PasswordResetEmail.php +++ b/src/app/Jobs/PasswordResetEmail.php @@ -7,7 +7,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Support\Facades\Mail; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; @@ -59,6 +58,10 @@ { $email = $this->code->user->getSetting('external_email'); - Mail::to($email)->send(new PasswordReset($this->code)); + \App\Mail\Helper::sendMail( + new PasswordReset($this->code), + $this->code->user->tenant_id, + ['to' => $email] + ); } } diff --git a/src/app/Jobs/PaymentEmail.php b/src/app/Jobs/PaymentEmail.php --- a/src/app/Jobs/PaymentEmail.php +++ b/src/app/Jobs/PaymentEmail.php @@ -8,7 +8,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Support\Facades\Mail; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; @@ -82,28 +81,13 @@ list($to, $cc) = \App\Mail\Helper::userEmails($this->controller); if (!empty($to)) { - try { - Mail::to($to)->cc($cc)->send($mail); - - $msg = sprintf( - "[Payment] %s mail sent for %s (%s)", - $label, - $wallet->id, - empty($cc) ? $to : implode(', ', array_merge([$to], $cc)) - ); - - \Log::info($msg); - } catch (\Exception $e) { - $msg = sprintf( - "[Payment] Failed to send mail for wallet %s (%s): %s", - $wallet->id, - empty($cc) ? $to : implode(', ', array_merge([$to], $cc)), - $e->getMessage() - ); - - \Log::error($msg); - throw $e; - } + $params = [ + 'to' => $to, + 'cc' => $cc, + 'add' => " for {$wallet->id}", + ]; + + \App\Mail\Helper::sendMail($mail, $this->controller->tenant_id, $params); } /* diff --git a/src/app/Jobs/PaymentMandateDisabledEmail.php b/src/app/Jobs/PaymentMandateDisabledEmail.php --- a/src/app/Jobs/PaymentMandateDisabledEmail.php +++ b/src/app/Jobs/PaymentMandateDisabledEmail.php @@ -8,7 +8,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Support\Facades\Mail; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; @@ -69,27 +68,13 @@ list($to, $cc) = \App\Mail\Helper::userEmails($this->controller); if (!empty($to)) { - try { - Mail::to($to)->cc($cc)->send($mail); - - $msg = sprintf( - "[PaymentMandateDisabled] Sent mail for %s (%s)", - $this->wallet->id, - empty($cc) ? $to : implode(', ', array_merge([$to], $cc)) - ); - - \Log::info($msg); - } catch (\Exception $e) { - $msg = sprintf( - "[PaymentMandateDisabled] Failed to send mail for wallet %s (%s): %s", - $this->wallet->id, - empty($cc) ? $to : implode(', ', array_merge([$to], $cc)), - $e->getMessage() - ); - - \Log::error($msg); - throw $e; - } + $params = [ + 'to' => $to, + 'cc' => $cc, + 'add' => " for {$this->wallet->id}", + ]; + + \App\Mail\Helper::sendMail($mail, $this->controller->tenant_id, $params); } /* diff --git a/src/app/Jobs/SignupInvitationEmail.php b/src/app/Jobs/SignupInvitationEmail.php --- a/src/app/Jobs/SignupInvitationEmail.php +++ b/src/app/Jobs/SignupInvitationEmail.php @@ -7,7 +7,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Support\Facades\Mail; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; @@ -50,7 +49,11 @@ */ public function handle() { - Mail::to($this->invitation->email)->send(new SignupInvitationMail($this->invitation)); + \App\Mail\Helper::sendMail( + new SignupInvitationMail($this->invitation), + $this->invitation->tenant_id, + ['to' => $this->invitation->email] + ); // Update invitation status $this->invitation->status = SignupInvitation::STATUS_SENT; diff --git a/src/app/Jobs/SignupVerificationEmail.php b/src/app/Jobs/SignupVerificationEmail.php --- a/src/app/Jobs/SignupVerificationEmail.php +++ b/src/app/Jobs/SignupVerificationEmail.php @@ -7,7 +7,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Support\Facades\Mail; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; @@ -58,6 +57,10 @@ */ public function handle() { - Mail::to($this->code->email)->send(new SignupVerification($this->code)); + \App\Mail\Helper::sendMail( + new SignupVerification($this->code), + null, + ['to' => $this->code->email] + ); } } diff --git a/src/app/Jobs/WalletCheck.php b/src/app/Jobs/WalletCheck.php --- a/src/app/Jobs/WalletCheck.php +++ b/src/app/Jobs/WalletCheck.php @@ -10,7 +10,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Mail; class WalletCheck implements ShouldQueue { @@ -121,9 +120,7 @@ // TODO: Should we check if the account is already suspended? - $label = "Notification sent for"; - - $this->sendMail(\App\Mail\NegativeBalance::class, false, $label); + $this->sendMail(\App\Mail\NegativeBalance::class, false); $now = \Carbon\Carbon::now()->toDateTimeString(); $this->wallet->setSetting('balance_warning_initial', $now); @@ -140,9 +137,7 @@ // TODO: Should we check if the account is already suspended? - $label = "Reminder sent for"; - - $this->sendMail(\App\Mail\NegativeBalanceReminder::class, false, $label); + $this->sendMail(\App\Mail\NegativeBalanceReminder::class, false); $now = \Carbon\Carbon::now()->toDateTimeString(); $this->wallet->setSetting('balance_warning_reminder', $now); @@ -173,9 +168,7 @@ } } - $label = "Account suspended"; - - $this->sendMail(\App\Mail\NegativeBalanceSuspended::class, true, $label); + $this->sendMail(\App\Mail\NegativeBalanceSuspended::class, true); $now = \Carbon\Carbon::now()->toDateTimeString(); $this->wallet->setSetting('balance_warning_suspended', $now); @@ -195,9 +188,7 @@ return; } - $label = "Last warning sent for"; - - $this->sendMail(\App\Mail\NegativeBalanceBeforeDelete::class, true, $label); + $this->sendMail(\App\Mail\NegativeBalanceBeforeDelete::class, true); $now = \Carbon\Carbon::now()->toDateTimeString(); $this->wallet->setSetting('balance_warning_before_delete', $now); @@ -232,9 +223,8 @@ * * @param string $class Mailable class name * @param bool $with_external Use users's external email - * @param ?string $log_label Log label */ - protected function sendMail($class, $with_external = false, $log_label = null): void + protected function sendMail($class, $with_external = false): void { // TODO: Send the email to all wallet controllers? @@ -243,30 +233,13 @@ list($to, $cc) = \App\Mail\Helper::userEmails($this->wallet->owner, $with_external); if (!empty($to) || !empty($cc)) { - try { - Mail::to($to)->cc($cc)->send($mail); - - if ($log_label) { - $msg = sprintf( - "[WalletCheck] %s %s (%s)", - $log_label, - $this->wallet->id, - empty($cc) ? $to : implode(', ', array_merge([$to], $cc)), - ); - - \Log::info($msg); - } - } catch (\Exception $e) { - $msg = sprintf( - "[WalletCheck] Failed to send mail for %s (%s): %s", - $this->wallet->id, - empty($cc) ? $to : implode(', ', array_merge([$to], $cc)), - $e->getMessage() - ); + $params = [ + 'to' => $to, + 'cc' => $cc, + 'add' => " for {$this->wallet->id}", + ]; - \Log::error($msg); - throw $e; - } + \App\Mail\Helper::sendMail($mail, $this->wallet->owner->tenant_id, $params); } } diff --git a/src/app/Mail/Helper.php b/src/app/Mail/Helper.php --- a/src/app/Mail/Helper.php +++ b/src/app/Mail/Helper.php @@ -2,7 +2,9 @@ namespace App\Mail; +use App\Tenant; use Illuminate\Mail\Mailable; +use Illuminate\Support\Facades\Mail; class Helper { @@ -31,6 +33,73 @@ return $mail->build()->render(); // @phpstan-ignore-line } + /** + * Sends an email + * + * @param Mailable $mail Email content generator + * @param int|null $tenantId Tenant identifier + * @param array $params Email parameters: to, cc + * + * @throws \Exception + */ + public static function sendMail(Mailable $mail, $tenantId = null, array $params = []): void + { + $class = explode("\\", get_class($mail)); + $class = end($class); + + $getRecipients = function () use ($params) { + $recipients = []; + + // For now we do not support addresses + names, only addresses + foreach (['to', 'cc'] as $idx) { + if (!empty($params[$idx])) { + if (is_array($params[$idx])) { + $recipients = array_merge($recipients, $params[$idx]); + } else { + $recipients[] = $params[$idx]; + } + } + } + + return implode(', ', $recipients); + }; + + try { + if (!empty($params['to'])) { + $mail->to($params['to']); + } + + if (!empty($params['cc'])) { + $mail->cc($params['cc']); + } + + $fromAddress = Tenant::getConfig($tenantId, 'mail.from.address'); + $fromName = Tenant::getConfig($tenantId, 'mail.from.name'); + $replytoAddress = Tenant::getConfig($tenantId, 'mail.reply_to.address'); + $replytoName = Tenant::getConfig($tenantId, 'mail.reply_to.name'); + + if ($fromAddress) { + $mail->from($fromAddress, $fromName); + } + + if ($replytoAddress) { + $mail->replyTo($replytoAddress, $replytoName); + } + + Mail::send($mail); + + $msg = sprintf("[%s] Sent mail to %s%s", $class, $getRecipients(), $params['add'] ?? ''); + + \Log::info($msg); + } catch (\Exception $e) { + $format = "[%s] Failed to send mail to %s%s: %s"; + $msg = sprintf($format, $class, $getRecipients(), $params['add'] ?? '', $e->getMessage()); + + \Log::error($msg); + throw $e; + } + } + /** * Return user's email addresses, separately for use in To and Cc. * diff --git a/src/app/Mail/NegativeBalance.php b/src/app/Mail/NegativeBalance.php --- a/src/app/Mail/NegativeBalance.php +++ b/src/app/Mail/NegativeBalance.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Tenant; use App\User; use App\Utils; use App\Wallet; @@ -42,17 +43,20 @@ */ public function build() { - $subject = \trans('mail.negativebalance-subject', ['site' => \config('app.name')]); + $appName = Tenant::getConfig($this->user->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->user->tenant_id, 'app.support_url'); + + $subject = \trans('mail.negativebalance-subject', ['site' => $appName]); $this->view('emails.html.negative_balance') ->text('emails.plain.negative_balance') ->subject($subject) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'subject' => $subject, 'username' => $this->user->name(true), - 'supportUrl' => \config('app.support_url'), - 'walletUrl' => Utils::serviceUrl('/wallet'), + 'supportUrl' => $supportUrl, + 'walletUrl' => Utils::serviceUrl('/wallet', $this->user->tenant_id), ]); return $this; diff --git a/src/app/Mail/NegativeBalanceBeforeDelete.php b/src/app/Mail/NegativeBalanceBeforeDelete.php --- a/src/app/Mail/NegativeBalanceBeforeDelete.php +++ b/src/app/Mail/NegativeBalanceBeforeDelete.php @@ -3,6 +3,7 @@ namespace App\Mail; use App\Jobs\WalletCheck; +use App\Tenant; use App\User; use App\Utils; use App\Wallet; @@ -44,18 +45,20 @@ public function build() { $threshold = WalletCheck::threshold($this->wallet, WalletCheck::THRESHOLD_DELETE); + $appName = Tenant::getConfig($this->user->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->user->tenant_id, 'app.support_url'); - $subject = \trans('mail.negativebalancebeforedelete-subject', ['site' => \config('app.name')]); + $subject = \trans('mail.negativebalancebeforedelete-subject', ['site' => $appName]); $this->view('emails.html.negative_balance_before_delete') ->text('emails.plain.negative_balance_before_delete') ->subject($subject) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'subject' => $subject, 'username' => $this->user->name(true), - 'supportUrl' => \config('app.support_url'), - 'walletUrl' => Utils::serviceUrl('/wallet'), + 'supportUrl' => $supportUrl, + 'walletUrl' => Utils::serviceUrl('/wallet', $this->user->tenant_id), 'date' => $threshold->toDateString(), ]); diff --git a/src/app/Mail/NegativeBalanceReminder.php b/src/app/Mail/NegativeBalanceReminder.php --- a/src/app/Mail/NegativeBalanceReminder.php +++ b/src/app/Mail/NegativeBalanceReminder.php @@ -3,6 +3,7 @@ namespace App\Mail; use App\Jobs\WalletCheck; +use App\Tenant; use App\User; use App\Utils; use App\Wallet; @@ -44,18 +45,20 @@ public function build() { $threshold = WalletCheck::threshold($this->wallet, WalletCheck::THRESHOLD_SUSPEND); + $appName = Tenant::getConfig($this->user->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->user->tenant_id, 'app.support_url'); - $subject = \trans('mail.negativebalancereminder-subject', ['site' => \config('app.name')]); + $subject = \trans('mail.negativebalancereminder-subject', ['site' => $appName]); $this->view('emails.html.negative_balance_reminder') ->text('emails.plain.negative_balance_reminder') ->subject($subject) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'subject' => $subject, 'username' => $this->user->name(true), - 'supportUrl' => \config('app.support_url'), - 'walletUrl' => Utils::serviceUrl('/wallet'), + 'supportUrl' => $supportUrl, + 'walletUrl' => Utils::serviceUrl('/wallet', $this->user->tenant_id), 'date' => $threshold->toDateString(), ]); diff --git a/src/app/Mail/NegativeBalanceSuspended.php b/src/app/Mail/NegativeBalanceSuspended.php --- a/src/app/Mail/NegativeBalanceSuspended.php +++ b/src/app/Mail/NegativeBalanceSuspended.php @@ -3,6 +3,7 @@ namespace App\Mail; use App\Jobs\WalletCheck; +use App\Tenant; use App\User; use App\Utils; use App\Wallet; @@ -44,18 +45,20 @@ public function build() { $threshold = WalletCheck::threshold($this->wallet, WalletCheck::THRESHOLD_DELETE); + $appName = Tenant::getConfig($this->user->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->user->tenant_id, 'app.support_url'); - $subject = \trans('mail.negativebalancesuspended-subject', ['site' => \config('app.name')]); + $subject = \trans('mail.negativebalancesuspended-subject', ['site' => $appName]); $this->view('emails.html.negative_balance_suspended') ->text('emails.plain.negative_balance_suspended') ->subject($subject) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'subject' => $subject, 'username' => $this->user->name(true), - 'supportUrl' => \config('app.support_url'), - 'walletUrl' => Utils::serviceUrl('/wallet'), + 'supportUrl' => $supportUrl, + 'walletUrl' => Utils::serviceUrl('/wallet', $this->user->tenant_id), 'date' => $threshold->toDateString(), ]); diff --git a/src/app/Mail/PasswordReset.php b/src/app/Mail/PasswordReset.php --- a/src/app/Mail/PasswordReset.php +++ b/src/app/Mail/PasswordReset.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Tenant; use App\User; use App\Utils; use App\VerificationCode; @@ -38,15 +39,19 @@ */ public function build() { + $appName = Tenant::getConfig($this->code->user->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->code->user->tenant_id, 'app.support_url'); + $href = Utils::serviceUrl( - sprintf('/password-reset/%s-%s', $this->code->short_code, $this->code->code) + sprintf('/password-reset/%s-%s', $this->code->short_code, $this->code->code), + $this->code->user->tenant_id ); $this->view('emails.html.password_reset') ->text('emails.plain.password_reset') - ->subject(__('mail.passwordreset-subject', ['site' => \config('app.name')])) + ->subject(\trans('mail.passwordreset-subject', ['site' => $appName])) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'code' => $this->code->code, 'short_code' => $this->code->short_code, 'link' => sprintf('%s', $href, $href), diff --git a/src/app/Mail/PaymentFailure.php b/src/app/Mail/PaymentFailure.php --- a/src/app/Mail/PaymentFailure.php +++ b/src/app/Mail/PaymentFailure.php @@ -3,6 +3,7 @@ namespace App\Mail; use App\Payment; +use App\Tenant; use App\User; use App\Utils; use Illuminate\Bus\Queueable; @@ -42,19 +43,20 @@ */ public function build() { - $user = $this->user; + $appName = Tenant::getConfig($this->user->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->user->tenant_id, 'app.support_url'); - $subject = \trans('mail.paymentfailure-subject', ['site' => \config('app.name')]); + $subject = \trans('mail.paymentfailure-subject', ['site' => $appName]); $this->view('emails.html.payment_failure') ->text('emails.plain.payment_failure') ->subject($subject) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'subject' => $subject, - 'username' => $user->name(true), - 'walletUrl' => Utils::serviceUrl('/wallet'), - 'supportUrl' => \config('app.support_url'), + 'username' => $this->user->name(true), + 'walletUrl' => Utils::serviceUrl('/wallet', $this->user->tenant_id), + 'supportUrl' => $supportUrl, ]); return $this; diff --git a/src/app/Mail/PaymentMandateDisabled.php b/src/app/Mail/PaymentMandateDisabled.php --- a/src/app/Mail/PaymentMandateDisabled.php +++ b/src/app/Mail/PaymentMandateDisabled.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Tenant; use App\User; use App\Utils; use App\Wallet; @@ -42,19 +43,20 @@ */ public function build() { - $user = $this->user; + $appName = Tenant::getConfig($this->user->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->user->tenant_id, 'app.support_url'); - $subject = \trans('mail.paymentmandatedisabled-subject', ['site' => \config('app.name')]); + $subject = \trans('mail.paymentmandatedisabled-subject', ['site' => $appName]); $this->view('emails.html.payment_mandate_disabled') ->text('emails.plain.payment_mandate_disabled') ->subject($subject) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'subject' => $subject, - 'username' => $user->name(true), - 'walletUrl' => Utils::serviceUrl('/wallet'), - 'supportUrl' => \config('app.support_url'), + 'username' => $this->user->name(true), + 'walletUrl' => Utils::serviceUrl('/wallet', $this->user->tenant_id), + 'supportUrl' => $supportUrl, ]); return $this; diff --git a/src/app/Mail/PaymentSuccess.php b/src/app/Mail/PaymentSuccess.php --- a/src/app/Mail/PaymentSuccess.php +++ b/src/app/Mail/PaymentSuccess.php @@ -3,6 +3,7 @@ namespace App\Mail; use App\Payment; +use App\Tenant; use App\User; use App\Utils; use Illuminate\Bus\Queueable; @@ -42,19 +43,20 @@ */ public function build() { - $user = $this->user; + $appName = Tenant::getConfig($this->user->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->user->tenant_id, 'app.support_url'); - $subject = \trans('mail.paymentsuccess-subject', ['site' => \config('app.name')]); + $subject = \trans('mail.paymentsuccess-subject', ['site' => $appName]); $this->view('emails.html.payment_success') ->text('emails.plain.payment_success') ->subject($subject) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'subject' => $subject, - 'username' => $user->name(true), - 'walletUrl' => Utils::serviceUrl('/wallet'), - 'supportUrl' => \config('app.support_url'), + 'username' => $this->user->name(true), + 'walletUrl' => Utils::serviceUrl('/wallet', $this->user->tenant_id), + 'supportUrl' => $supportUrl, ]); return $this; diff --git a/src/app/Mail/SignupInvitation.php b/src/app/Mail/SignupInvitation.php --- a/src/app/Mail/SignupInvitation.php +++ b/src/app/Mail/SignupInvitation.php @@ -3,6 +3,7 @@ namespace App\Mail; use App\SignupInvitation as SI; +use App\Tenant; use App\Utils; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; @@ -37,13 +38,15 @@ */ public function build() { - $href = Utils::serviceUrl('/signup/invite/' . $this->invitation->id); + $appName = Tenant::getConfig($this->invitation->tenant_id, 'app.name'); + + $href = Utils::serviceUrl('/signup/invite/' . $this->invitation->id, $this->invitation->tenant_id); $this->view('emails.html.signup_invitation') ->text('emails.plain.signup_invitation') - ->subject(__('mail.signupinvitation-subject', ['site' => \config('app.name')])) + ->subject(\trans('mail.signupinvitation-subject', ['site' => $appName])) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'href' => $href, ]); diff --git a/src/app/Mail/SignupVerification.php b/src/app/Mail/SignupVerification.php --- a/src/app/Mail/SignupVerification.php +++ b/src/app/Mail/SignupVerification.php @@ -49,7 +49,7 @@ $this->view('emails.html.signup_code') ->text('emails.plain.signup_code') - ->subject(__('mail.signupcode-subject', ['site' => \config('app.name')])) + ->subject(\trans('mail.signupcode-subject', ['site' => \config('app.name')])) ->with([ 'site' => \config('app.name'), 'username' => $username ?: 'User', diff --git a/src/app/Mail/SuspendedDebtor.php b/src/app/Mail/SuspendedDebtor.php --- a/src/app/Mail/SuspendedDebtor.php +++ b/src/app/Mail/SuspendedDebtor.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Tenant; use App\User; use App\Utils; use Illuminate\Bus\Queueable; @@ -36,13 +37,15 @@ */ public function build() { - $user = $this->account; + $appName = Tenant::getConfig($this->account->tenant_id, 'app.name'); + $supportUrl = Tenant::getConfig($this->account->tenant_id, 'app.support_url'); + $cancelUrl = Tenant::getConfig($this->account->tenant_id, 'app.kb.account_delete'); - $subject = \trans('mail.suspendeddebtor-subject', ['site' => \config('app.name')]); + $subject = \trans('mail.suspendeddebtor-subject', ['site' => $appName]); $moreInfoHtml = null; $moreInfoText = null; - if ($moreInfoUrl = \config('app.kb.account_suspended')) { + if ($moreInfoUrl = Tenant::getConfig($this->account->tenant_id, 'app.kb.account_suspended')) { $moreInfoHtml = \trans('mail.more-info-html', ['href' => $moreInfoUrl]); $moreInfoText = \trans('mail.more-info-text', ['href' => $moreInfoUrl]); } @@ -51,12 +54,12 @@ ->text('emails.plain.suspended_debtor') ->subject($subject) ->with([ - 'site' => \config('app.name'), + 'site' => $appName, 'subject' => $subject, - 'username' => $user->name(true), - 'cancelUrl' => \config('app.kb.account_delete'), - 'supportUrl' => \config('app.support_url'), - 'walletUrl' => Utils::serviceUrl('/wallet'), + 'username' => $this->account->name(true), + 'cancelUrl' => $cancelUrl, + 'supportUrl' => $supportUrl, + 'walletUrl' => Utils::serviceUrl('/wallet', $this->account->tenant_id), 'moreInfoHtml' => $moreInfoHtml, 'moreInfoText' => $moreInfoText, 'days' => 14 // TODO: Configurable diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -82,20 +82,7 @@ Builder::macro( 'withObjectTenantContext', function (object $object, string $table = null) { - // backend artisan cli - if (app()->runningInConsole()) { - /** @var Builder $this */ - return $this->where(($table ? "$table." : "") . "tenant_id", $object->tenant_id); - } - - $subject = auth()->user(); - - if ($subject->role == "admin") { - /** @var Builder $this */ - return $this->where(($table ? "$table." : "") . "tenant_id", $object->tenant_id); - } - - $tenantId = $subject->tenant_id; + $tenantId = $object->tenant_id; if ($tenantId) { /** @var Builder $this */ diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php --- a/src/app/Providers/Payment/Mollie.php +++ b/src/app/Providers/Payment/Mollie.php @@ -470,13 +470,12 @@ */ protected static function mollieMandate(Wallet $wallet) { - $customer_id = $wallet->getSetting('mollie_id'); - $mandate_id = $wallet->getSetting('mollie_mandate_id'); + $settings = $wallet->getSettings(['mollie_id', 'mollie_mandate_id']); // Get the manadate reference we already have - if ($customer_id && $mandate_id) { + if ($settings['mollie_id'] && $settings['mollie_mandate_id']) { try { - return mollie()->mandates()->getForId($customer_id, $mandate_id); + return mollie()->mandates()->getForId($settings['mollie_id'], $settings['mollie_mandate_id']); } catch (ApiException $e) { // FIXME: What about 404? if ($e->getCode() == 410) { diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php --- a/src/app/Providers/PaymentProvider.php +++ b/src/app/Providers/PaymentProvider.php @@ -49,9 +49,11 @@ private static function providerName($provider_or_wallet = null): string { if ($provider_or_wallet instanceof Wallet) { - if ($provider_or_wallet->getSetting('stripe_id')) { + $settings = $provider_or_wallet->getSettings(['stripe_id', 'mollie_id']); + + if ($settings['stripe_id']) { $provider = self::PROVIDER_STRIPE; - } elseif ($provider_or_wallet->getSetting('mollie_id')) { + } elseif ($settings['mollie_id']) { $provider = self::PROVIDER_MOLLIE; } } else { diff --git a/src/app/Tenant.php b/src/app/Tenant.php --- a/src/app/Tenant.php +++ b/src/app/Tenant.php @@ -2,6 +2,7 @@ namespace App; +use App\Traits\SettingsTrait; use Illuminate\Database\Eloquent\Model; /** @@ -12,6 +13,8 @@ */ class Tenant extends Model { + use SettingsTrait; + protected $fillable = [ 'id', 'title', @@ -19,6 +22,44 @@ protected $keyType = 'bigint'; + /** + * Utility method to get tenant-specific system setting. + * If the setting is not specified for the tenant a system-wide value will be returned. + * + * @param int $tenantId Tenant identifier + * @param string $key Setting name + * + * @return mixed Setting value + */ + public static function getConfig($tenantId, string $key) + { + // Cache the tenant instance in memory + static $tenant; + + if (empty($tenant) || $tenant->id != $tenantId) { + $tenant = null; + if ($tenantId) { + $tenant = self::findOrFail($tenantId); + } + } + + // Supported options (TODO: document this somewhere): + // - app.name (tenants.title will be returned) + // - app.public_url and app.url + // - app.support_url + // - mail.from.address and mail.from.name + // - mail.reply_to.address and mail.reply_to.name + // - app.kb.account_delete and app.kb.account_suspended + + if ($key == 'app.name') { + return $tenant ? $tenant->title : \config($key); + } + + $value = $tenant ? $tenant->getSetting($key) : null; + + return $value !== null ? $value : \config($key); + } + /** * Discounts assigned to this tenant. * @@ -29,6 +70,16 @@ return $this->hasMany('App\Discount'); } + /** + * Any (additional) settings of this tenant. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function settings() + { + return $this->hasMany('App\TenantSetting'); + } + /** * SignupInvitations assigned to this tenant. * diff --git a/src/app/TenantSetting.php b/src/app/TenantSetting.php new file mode 100644 --- /dev/null +++ b/src/app/TenantSetting.php @@ -0,0 +1,30 @@ +belongsTo('\App\Tenant', 'tenant_id', 'id'); + } +} diff --git a/src/app/Traits/SettingsTrait.php b/src/app/Traits/SettingsTrait.php --- a/src/app/Traits/SettingsTrait.php +++ b/src/app/Traits/SettingsTrait.php @@ -2,8 +2,6 @@ namespace App\Traits; -use Illuminate\Support\Facades\Cache; - trait SettingsTrait { /** @@ -16,21 +14,39 @@ * $locale = $user->getSetting('locale'); * ``` * - * @param string $key Setting name + * @param string $key Setting name + * @param mixed $default Default value, to be used if not found * * @return string|null Setting value */ - public function getSetting(string $key) + public function getSetting(string $key, $default = null) + { + $setting = $this->settings()->where('key', $key)->first(); + + return $setting ? $setting->value : $default; + } + + /** + * Obtain the values for many settings in one go (for better performance). + * + * @param array $keys Setting names + * + * @return array Setting key=value hash, includes also requested but non-existing settings + */ + public function getSettings(array $keys): array { - $settings = $this->getCache(); + $settings = []; - if (!array_key_exists($key, $settings)) { - return null; + foreach ($keys as $key) { + $settings[$key] = null; } - $value = $settings[$key]; + $this->settings()->whereIn('key', $keys)->get() + ->each(function ($setting) use (&$settings) { + $settings[$setting->key] = $setting->value; + }); - return empty($value) ? null : $value; + return $settings; } /** @@ -70,7 +86,6 @@ public function setSetting(string $key, $value): void { $this->storeSetting($key, $value); - $this->setCache(); } /** @@ -92,10 +107,16 @@ foreach ($data as $key => $value) { $this->storeSetting($key, $value); } - - $this->setCache(); } + /** + * Create or update a setting. + * + * @param string $key Setting name + * @param string|null $value The new value for the setting. + * + * @return void + */ private function storeSetting(string $key, $value): void { if ($value === null || $value === '') { @@ -110,35 +131,4 @@ ); } } - - private function getCache() - { - $model = \strtolower(get_class($this)); - - if (Cache::has("{$model}_settings_{$this->id}")) { - return Cache::get("{$model}_settings_{$this->id}"); - } - - return $this->setCache(); - } - - private function setCache() - { - $model = \strtolower(get_class($this)); - - if (Cache::has("{$model}_settings_{$this->id}")) { - Cache::forget("{$model}_settings_{$this->id}"); - } - - $cached = []; - foreach ($this->settings()->get() as $entry) { - if ($entry->value !== null && $entry->value !== '') { - $cached[$entry->key] = $entry->value; - } - } - - Cache::forever("{$model}_settings_{$this->id}", $cached); - - return $this->getCache(); - } } diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -543,13 +543,12 @@ */ public function name(bool $fallback = false): string { - $firstname = $this->getSetting('first_name'); - $lastname = $this->getSetting('last_name'); + $settings = $this->getSettings(['first_name', 'last_name']); - $name = trim($firstname . ' ' . $lastname); + $name = trim($settings['first_name'] . ' ' . $settings['last_name']); if (empty($name) && $fallback) { - return \config('app.name') . ' User'; + return trim(\trans('app.siteuser', ['site' => \App\Tenant::getConfig($this->tenant_id, 'app.name')])); } return $name; diff --git a/src/app/Utils.php b/src/app/Utils.php --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -367,17 +367,19 @@ /** * Create self URL * - * @param string $route Route/Path + * @param string $route Route/Path + * @param int|null $tenantId Current tenant + * * @todo Move this to App\Http\Controllers\Controller * * @return string Full URL */ - public static function serviceUrl(string $route): string + public static function serviceUrl(string $route, $tenantId = null): string { - $url = \config('app.public_url'); + $url = \App\Tenant::getConfig($tenantId, 'app.public_url'); if (!$url) { - $url = \config('app.url'); + $url = \App\Tenant::getConfig($tenantId, 'app.url'); } return rtrim(trim($url, '/') . '/' . ltrim($route, '/'), '/'); diff --git a/src/database/migrations/2021_07_12_100000_create_tenant_settings_table.php b/src/database/migrations/2021_07_12_100000_create_tenant_settings_table.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2021_07_12_100000_create_tenant_settings_table.php @@ -0,0 +1,45 @@ +bigIncrements('id'); + $table->unsignedBigInteger('tenant_id'); + $table->string('key'); + $table->text('value'); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->useCurrent(); + + $table->foreign('tenant_id')->references('id')->on('tenants') + ->onDelete('cascade')->onUpdate('cascade'); + + $table->unique(['tenant_id', 'key']); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('tenant_settings'); + } +} diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php --- a/src/resources/lang/en/app.php +++ b/src/resources/lang/en/app.php @@ -66,6 +66,8 @@ 'support-request-success' => 'Support request submitted successfully.', 'support-request-error' => 'Failed to submit the support request.', + 'siteuser' => ':site User', + 'wallet-award-success' => 'The bonus has been added to the wallet successfully.', 'wallet-penalty-success' => 'The penalty has been added to the wallet successfully.', 'wallet-update-success' => 'User wallet updated successfully.', diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php --- a/src/tests/Browser/Meet/RoomControlsTest.php +++ b/src/tests/Browser/Meet/RoomControlsTest.php @@ -167,7 +167,7 @@ // Test muting audio $owner->click('@menu button.link-audio') ->assertToolbarButtonState('audio', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED) - ->assertVisible('div.meet-video.self .status .status-audio'); + ->waitFor('div.meet-video.self .status .status-audio'); // FIXME: It looks that we can't just check the