diff --git a/app/Http/Controllers/V1/Admin/Settings/MailConfigurationController.php b/app/Http/Controllers/V1/Admin/Settings/MailConfigurationController.php index 29794d5c..f07029dc 100755 --- a/app/Http/Controllers/V1/Admin/Settings/MailConfigurationController.php +++ b/app/Http/Controllers/V1/Admin/Settings/MailConfigurationController.php @@ -3,80 +3,29 @@ namespace Crater\Http\Controllers\V1\Admin\Settings; use Crater\Http\Controllers\Controller; -use Crater\Http\Requests\MailEnvironmentRequest; +use Crater\Http\Requests\TestMailDriverRequest; use Crater\Mail\TestMail; -use Crater\Models\Setting; -use Crater\Space\EnvironmentManager; -use Illuminate\Http\JsonResponse; +use Crater\Models\MailSender; use Illuminate\Http\Request; use Mail; class MailConfigurationController extends Controller { - /** - * @var EnvironmentManager - */ - protected $environmentManager; - - /** - * @param EnvironmentManager $environmentManager - */ - public function __construct(EnvironmentManager $environmentManager) - { - $this->environmentManager = $environmentManager; - } - - /** - * - * @param MailEnvironmentRequest $request - * @return JsonResponse - */ - public function saveMailEnvironment(MailEnvironmentRequest $request) + public function TestMailDriver(TestMailDriverRequest $request) { $this->authorize('manage email config'); - $setting = Setting::getSetting('profile_complete'); - $results = $this->environmentManager->saveMailVariables($request); + MailSender::setMailConfiguration($request->mail_sender_id); - if ($setting !== 'COMPLETED') { - Setting::setSetting('profile_complete', 4); - } + Mail::to($request->to)->send(new TestMail($request->subject, $request->message)); - return response()->json($results); + return response()->json([ + 'success' => true, + ]); } - public function getMailEnvironment() + public function getMailDrivers(Request $request) { - $this->authorize('manage email config'); - - $MailData = [ - 'mail_driver' => config('mail.driver'), - 'mail_host' => config('mail.host'), - 'mail_port' => config('mail.port'), - 'mail_username' => config('mail.username'), - 'mail_password' => config('mail.password'), - 'mail_encryption' => config('mail.encryption'), - 'from_name' => config('mail.from.name'), - 'from_mail' => config('mail.from.address'), - 'mail_mailgun_endpoint' => config('services.mailgun.endpoint'), - 'mail_mailgun_domain' => config('services.mailgun.domain'), - 'mail_mailgun_secret' => config('services.mailgun.secret'), - 'mail_ses_key' => config('services.ses.key'), - 'mail_ses_secret' => config('services.ses.secret'), - ]; - - - return response()->json($MailData); - } - - /** - * - * @return JsonResponse - */ - public function getMailDrivers() - { - $this->authorize('manage email config'); - $drivers = [ 'smtp', 'mail', @@ -87,21 +36,4 @@ class MailConfigurationController extends Controller return response()->json($drivers); } - - public function testEmailConfig(Request $request) - { - $this->authorize('manage email config'); - - $this->validate($request, [ - 'to' => 'required|email', - 'subject' => 'required', - 'message' => 'required', - ]); - - Mail::to($request->to)->send(new TestMail($request->subject, $request->message)); - - return response()->json([ - 'success' => true, - ]); - } } diff --git a/app/Http/Requests/MailSenderRequest.php b/app/Http/Requests/MailSenderRequest.php index 70be59f6..44e64a2f 100644 --- a/app/Http/Requests/MailSenderRequest.php +++ b/app/Http/Requests/MailSenderRequest.php @@ -14,7 +14,7 @@ class MailSenderRequest extends FormRequest */ public function authorize() { - return false; + return true; } /** diff --git a/app/Http/Requests/TestMailDriverRequest.php b/app/Http/Requests/TestMailDriverRequest.php new file mode 100644 index 00000000..b3bc9e2e --- /dev/null +++ b/app/Http/Requests/TestMailDriverRequest.php @@ -0,0 +1,39 @@ + [ + 'required', + 'email' + ], + 'subject' => [ + 'required' + ], + 'message' => [ + 'required' + ], + ]; + } +} diff --git a/app/Models/MailSender.php b/app/Models/MailSender.php index 61534a12..625baaf6 100644 --- a/app/Models/MailSender.php +++ b/app/Models/MailSender.php @@ -5,6 +5,7 @@ namespace Crater\Models; use Crater\Http\Requests\MailSenderRequest; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Config; class MailSender extends Model { @@ -74,4 +75,25 @@ class MailSender extends Model return $this; } + + public static function setMailConfiguration($id, $check = null) + { + $mailSender = MailSender::find($id); + + $settings = $mailSender->settings; + $settings['driver'] = $mailSender->driver; + $settings['from'] = [ + 'address' => $mailSender->from_address, + 'name' => $mailSender->from_name + ]; + + Config::set('mail', $settings); + + if ($check) { + return $mailSender; + } + + return true; + } + } diff --git a/app/Policies/MailSenderPolicy.php b/app/Policies/MailSenderPolicy.php new file mode 100644 index 00000000..079afe75 --- /dev/null +++ b/app/Policies/MailSenderPolicy.php @@ -0,0 +1,123 @@ +hasCompany($mailSender->company_id)) { + return true; + } + + return false; + } + + /** + * Determine whether the user can create models. + * + * @param \Crater\Models\User $user + * @return \Illuminate\Auth\Access\Response|bool + */ + public function create(User $user) + { + if (BouncerFacade::can('create-mail-sender', MailSender::class)) { + return true; + } + + return false; + } + + /** + * Determine whether the user can update the model. + * + * @param \Crater\Models\User $user + * @param \Crater\Models\MailSender $mailSender + * @return \Illuminate\Auth\Access\Response|bool + */ + public function update(User $user, MailSender $mailSender) + { + if (BouncerFacade::can('edit-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) { + return true; + } + + return false; + } + + /** + * Determine whether the user can delete the model. + * + * @param \Crater\Models\User $user + * @param \Crater\Models\MailSender $mailSender + * @return \Illuminate\Auth\Access\Response|bool + */ + public function delete(User $user, MailSender $mailSender) + { + if (BouncerFacade::can('delete-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) { + return true; + } + + return false; + } + + /** + * Determine whether the user can restore the model. + * + * @param \Crater\Models\User $user + * @param \Crater\Models\MailSender $mailSender + * @return \Illuminate\Auth\Access\Response|bool + */ + public function restore(User $user, MailSender $mailSender) + { + if (BouncerFacade::can('delete-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) { + return true; + } + + return false; + } + + /** + * Determine whether the user can permanently delete the model. + * + * @param \Crater\Models\User $user + * @param \Crater\Models\MailSender $mailSender + * @return \Illuminate\Auth\Access\Response|bool + */ + public function forceDelete(User $user, MailSender $mailSender) + { + if (BouncerFacade::can('delete-mail-sender', $mailSender) && $user->hasCompany($mailSender->company_id)) { + return true; + } + + return false; + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 61cd4305..63a6d28a 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -39,6 +39,7 @@ class AuthServiceProvider extends ServiceProvider \Crater\Models\CustomField::class => \Crater\Policies\CustomFieldPolicy::class, \Crater\Models\User::class => \Crater\Policies\UserPolicy::class, \Crater\Models\Item::class => \Crater\Policies\ItemPolicy::class, + \Crater\Models\MailSender::class => \Crater\Policies\MailSenderPolicy::class, \Silber\Bouncer\Database\Role::class => \Crater\Policies\RolePolicy::class, \Crater\Models\Unit::class => \Crater\Policies\UnitPolicy::class, \Crater\Models\RecurringInvoice::class => \Crater\Policies\RecurringInvoicePolicy::class, diff --git a/config/crater.php b/config/crater.php index 4de98105..894ba767 100644 --- a/config/crater.php +++ b/config/crater.php @@ -7,6 +7,7 @@ use Crater\Models\ExchangeRateProvider; use Crater\Models\Expense; use Crater\Models\Invoice; use Crater\Models\Item; +use Crater\Models\MailSender; use Crater\Models\Note; use Crater\Models\Payment; use Crater\Models\RecurringInvoice; @@ -225,6 +226,17 @@ return [ 'ability' => 'view-all-notes', 'model' => Note::class ], + [ + 'title' => 'settings.menu_title.mail_sender', + 'group' => '', + 'name' => 'Mail Sender', + 'link' => '/admin/settings/mail-sender', + 'icon' => 'MailIcon', + 'owner_only' => false, + 'ability' => 'view-mail-sender', + 'model' => MailSender::class + ], + [ 'title' => 'settings.menu_title.expense_category', 'group' => '', @@ -235,16 +247,6 @@ return [ 'ability' => 'view-expense', 'model' => Expense::class ], - [ - 'title' => 'settings.mail.mail_config', - 'group' => '', - 'name' => 'Mail Configuration', - 'link' => '/admin/settings/mail-configuration', - 'icon' => 'MailIcon', - 'owner_only' => true, - 'ability' => '', - 'model' => '' - ], [ 'title' => 'settings.menu_title.file_disk', 'group' => '', @@ -275,6 +277,7 @@ return [ 'ability' => '', 'model' => '' ], + ], /* diff --git a/resources/scripts/admin/admin-router.js b/resources/scripts/admin/admin-router.js index d11952e4..15de1d40 100644 --- a/resources/scripts/admin/admin-router.js +++ b/resources/scripts/admin/admin-router.js @@ -47,8 +47,6 @@ const ExpenseCategory = () => import('@/scripts/admin/views/settings/ExpenseCategorySetting.vue') const ExchangeRateSetting = () => import('@/scripts/admin/views/settings/ExchangeRateProviderSetting.vue') -const MailConfig = () => - import('@/scripts/admin/views/settings/MailConfigSetting.vue') const FileDisk = () => import('@/scripts/admin/views/settings/FileDiskSetting.vue') const Backup = () => import('@/scripts/admin/views/settings/BackupSetting.vue') @@ -56,6 +54,8 @@ const UpdateApp = () => import('@/scripts/admin/views/settings/UpdateAppSetting.vue') const RolesSettings = () => import('@/scripts/admin/views/settings/RolesSettings.vue') +const MailSender = () => + import('@/scripts/admin/views/settings/mail-sender/Index.vue') // Items const ItemsIndex = () => import('@/scripts/admin/views/items/Index.vue') @@ -302,13 +302,6 @@ export default [ meta: { ability: abilities.VIEW_EXPENSE }, component: ExpenseCategory, }, - - { - path: 'mail-configuration', - name: 'mailconfig', - meta: { isOwner: true }, - component: MailConfig, - }, { path: 'file-disk', name: 'file-disk', @@ -327,6 +320,13 @@ export default [ meta: { isOwner: true }, component: UpdateApp, }, + { + path: 'mail-sender', + name: 'mailsender', + meta: { ability: abilities.VIEW_MAIL_SENDER }, + component: MailSender, + }, + ], }, diff --git a/resources/scripts/admin/components/dropdowns/MailSenderIndexDropdown.vue b/resources/scripts/admin/components/dropdowns/MailSenderIndexDropdown.vue new file mode 100644 index 00000000..365382b3 --- /dev/null +++ b/resources/scripts/admin/components/dropdowns/MailSenderIndexDropdown.vue @@ -0,0 +1,111 @@ + + + + + + + + + + + + + {{ $t('general.edit') }} + + + + + + {{ $t('general.delete') }} + + + + + + {{ $t('general.send_test_mail') }} + + + + + diff --git a/resources/scripts/admin/components/modal-components/MailSenderModal.vue b/resources/scripts/admin/components/modal-components/MailSenderModal.vue new file mode 100644 index 00000000..d1532644 --- /dev/null +++ b/resources/scripts/admin/components/modal-components/MailSenderModal.vue @@ -0,0 +1,290 @@ + + + + + {{ modalStore.title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('general.cancel') }} + + + + + + {{ + mailSenderStore.isEdit ? $t('general.update') : $t('general.save') + }} + + + + + + + diff --git a/resources/scripts/admin/components/modal-components/MailSenderTestModal.vue b/resources/scripts/admin/components/modal-components/MailSenderTestModal.vue new file mode 100644 index 00000000..06c4e39a --- /dev/null +++ b/resources/scripts/admin/components/modal-components/MailSenderTestModal.vue @@ -0,0 +1,208 @@ + + + + + {{ modalStore.title }} + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('general.cancel') }} + + + + + + + {{ $t('general.send') }} + + + + + + + diff --git a/resources/scripts/admin/stores/mail-sender.js b/resources/scripts/admin/stores/mail-sender.js new file mode 100644 index 00000000..588d5241 --- /dev/null +++ b/resources/scripts/admin/stores/mail-sender.js @@ -0,0 +1,209 @@ +import axios from 'axios' +import { defineStore } from 'pinia' +import { useNotificationStore } from '@/scripts/stores/notification' +import { handleError } from '@/scripts/helpers/error-handling' +import mailSenderStub from '@/scripts/admin/stub/mail-sender.js' + +export const useMailSenderStore = (useWindow = false) => { + const pre_t = 'settings.mail_sender' + const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore + const { global } = window.i18n + + return defineStoreFunc({ + id: 'mailSender', + + state: () => ({ + mailSenders: [], + mail_drivers: [], // list of mail drivers + currentMailSender: { ...mailSenderStub.basicConfig }, + smtpConfig: { ...mailSenderStub.smtpConfig }, + mailgunConfig: { ...mailSenderStub.mailgunConfig }, + sesConfig: { ...mailSenderStub.sesConfig }, + mail_encryptions: ['none', 'tls', 'ssl', 'starttls'], + }), + + getters: { + isEdit: (state) => (state.currentMailSender.id ? true : false), + }, + + actions: { + resetCurrentMailSender() { + this.currentMailSender = { ...mailSenderStub.basicConfig } + this.smtpConfig = { ...mailSenderStub.smtpConfig } + this.mailgunConfig = { ...mailSenderStub.mailgunConfig } + this.sesConfig = { ...mailSenderStub.sesConfig } + }, + + fetchMailDrivers() { + return new Promise((resolve, reject) => { + axios + .get('/api/v1/mail-drivers') + .then((response) => { + if (response.data) { + this.mail_drivers = response.data + } + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + + fetchMailSenderList(params) { + return new Promise((resolve, reject) => { + axios + .get(`/api/v1/mail-senders`, { params }) + .then((response) => { + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + + fetchMailSenders(params) { + return new Promise((resolve, reject) => { + axios + .get(`/api/v1/mail-sender`, { params }) + .then((response) => { + this.mailSenders = response.data.data + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + + fetchMailSender(id) { + return new Promise((resolve, reject) => { + axios + .get(`/api/v1/mail-sender/${id}`) + .then((response) => { + this.currentMailSender = response.data.data + if (response.data.data.settings) { + var settings = response.data.data.settings + switch (response.data.data.driver) { + case 'smtp': + this.smtpConfig = settings + break + case 'mailgun': + this.mailgunConfig = settings + break + case 'ses': + this.sesConfig = settings + break + } + } + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + + addMailSender(data) { + const notificationStore = useNotificationStore() + return new Promise((resolve, reject) => { + axios + .post('/api/v1/mail-sender', data) + .then((response) => { + this.mailSenders.push(response.data.data) + notificationStore.showNotification({ + type: 'success', + message: global.t(`${pre_t}.created_message`), + }) + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + + updateMailSender(data) { + const notificationStore = useNotificationStore() + return new Promise((resolve, reject) => { + axios + .put(`/api/v1/mail-sender/${data.id}`, data) + .then((response) => { + if (response.data) { + let pos = this.mailSenders.findIndex( + (mailSender) => mailSender.id === response.data.data.id + ) + this.mailSenders[pos] = data + notificationStore.showNotification({ + type: 'success', + message: global.t(`${pre_t}.updated_message`), + }) + } + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + + deleteMailSender(id) { + return new Promise((resolve, reject) => { + axios + .delete(`/api/v1/mail-sender/${id}`) + .then((response) => { + if (response.data.success) { + let index = this.mailSenders.findIndex( + (mailSender) => mailSender.id === id + ) + this.mailSenders.splice(index, 1) + const notificationStore = useNotificationStore() + notificationStore.showNotification({ + type: 'success', + message: global.t(`${pre_t}.deleted_message`), + }) + } + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + + sendTestMail(data) { + return new Promise((resolve, reject) => { + axios + .post('/api/v1/mail-test', data) + .then((response) => { + const notificationStore = useNotificationStore() + if (response.data.success) { + notificationStore.showNotification({ + type: 'success', + message: global.t('general.send_mail_successfully'), + }) + } else { + notificationStore.showNotification({ + type: 'error', + message: global.t('validation.something_went_wrong'), + }) + } + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + }, + })() +} diff --git a/resources/scripts/admin/stores/users.js b/resources/scripts/admin/stores/users.js index 30a5bfd4..8e337f70 100644 --- a/resources/scripts/admin/stores/users.js +++ b/resources/scripts/admin/stores/users.js @@ -25,6 +25,7 @@ export const useUsersStore = (useWindow = false) => { password: null, phone: null, companies: [], + sender_id: null, }, }), diff --git a/resources/scripts/admin/stub/customer.js b/resources/scripts/admin/stub/customer.js index 55512add..512c9641 100644 --- a/resources/scripts/admin/stub/customer.js +++ b/resources/scripts/admin/stub/customer.js @@ -15,5 +15,6 @@ export default function () { customFields: [], fields: [], enable_portal: false, + mail_sender_id: null, } } diff --git a/resources/scripts/admin/stub/mail-sender.js b/resources/scripts/admin/stub/mail-sender.js new file mode 100644 index 00000000..3e608a18 --- /dev/null +++ b/resources/scripts/admin/stub/mail-sender.js @@ -0,0 +1,31 @@ +export default { + basicConfig: { + name: '', + from_name: '', + from_address: '', + cc: '', + bcc: '', + is_default: false, + driver: 'smtp', // 'smtp', 'mail', 'sendmail', 'mailgun', 'ses' + settings: '', + }, + smtpConfig: { + host: '', + port: null, + username: '', + password: '', + encryption: 'tls', // 'tls', 'ssl', 'starttls' + }, + mailgunConfig: { + domain: '', + secret: '', + endpoint: '', + }, + sesConfig: { + host: '', + port: null, + encryption: 'tls', // 'tls', 'ssl', 'starttls' + ses_key: '', + ses_secret: '', + }, +} diff --git a/resources/scripts/admin/views/customers/Create.vue b/resources/scripts/admin/views/customers/Create.vue index a40a2a6b..eea559ae 100644 --- a/resources/scripts/admin/views/customers/Create.vue +++ b/resources/scripts/admin/views/customers/Create.vue @@ -256,6 +256,31 @@ /> + + + + @@ -601,11 +626,13 @@ import CustomerCustomFields from '@/scripts/admin/components/custom-fields/Creat import { useGlobalStore } from '@/scripts/admin/stores/global' import CopyInputField from '@/scripts/admin/components/CopyInputField.vue' import { useCompanyStore } from '@/scripts/admin/stores/company' +import { useMailSenderStore } from '@/scripts/admin/stores/mail-sender' const customerStore = useCustomerStore() const customFieldStore = useCustomFieldStore() const globalStore = useGlobalStore() const companyStore = useCompanyStore() +const mailSenderStore = useMailSenderStore() const customFieldValidationScope = 'customFields' @@ -617,6 +644,7 @@ const route = useRoute() let isFetchingInitialData = ref(false) let isShowPassword = ref(false) let isShowConfirmPassword = ref(false) +const mailSenders = ref(null) let active = ref(false) const isSaving = ref(false) @@ -679,6 +707,9 @@ const rules = computed(() => { website: { url: helpers.withMessage(t('validation.invalid_url'), url), }, + mail_sender_id: { + required: helpers.withMessage(t('validation.required'), required), + }, billing: { address_street_1: { maxLength: helpers.withMessage( @@ -722,6 +753,20 @@ const v$ = useVuelidate(rules, customerStore, { $scope: customFieldValidationScope, }) +onMounted(async () => { + await mailSenderStore.fetchMailSenders() + let mailSenderData = await mailSenderStore.fetchMailSenderList() + if (mailSenderData.data) { + mailSenders.value = mailSenderData.data.data + let defaultMailSender = mailSenderData.data.data.find( + (mailSender) => mailSender.is_default == true + ) + customerStore.currentCustomer.mail_sender_id = defaultMailSender + ? defaultMailSender.id + : null + } +}) + customerStore.resetCurrentCustomer() customerStore.fetchCustomerInitialSettings(isEdit.value) diff --git a/resources/scripts/admin/views/settings/mail-sender/Index.vue b/resources/scripts/admin/views/settings/mail-sender/Index.vue new file mode 100644 index 00000000..d46cf160 --- /dev/null +++ b/resources/scripts/admin/views/settings/mail-sender/Index.vue @@ -0,0 +1,136 @@ + + + + + + + + + + + {{ $t(`${pre_t}.add_new_mail_sender`) }} + + + + + + + {{ row.data.is_default ? $t('general.yes') : $t('general.no') }} + + + + + + + + + + + diff --git a/resources/scripts/admin/views/settings/mail-sender/MailgunDriver.vue b/resources/scripts/admin/views/settings/mail-sender/MailgunDriver.vue new file mode 100644 index 00000000..1889c9f0 --- /dev/null +++ b/resources/scripts/admin/views/settings/mail-sender/MailgunDriver.vue @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/scripts/admin/views/settings/mail-sender/SesDriver.vue b/resources/scripts/admin/views/settings/mail-sender/SesDriver.vue new file mode 100644 index 00000000..454bdda6 --- /dev/null +++ b/resources/scripts/admin/views/settings/mail-sender/SesDriver.vue @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/scripts/admin/views/settings/mail-sender/SmtpDriver.vue b/resources/scripts/admin/views/settings/mail-sender/SmtpDriver.vue new file mode 100644 index 00000000..36a387b0 --- /dev/null +++ b/resources/scripts/admin/views/settings/mail-sender/SmtpDriver.vue @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/scripts/admin/views/users/Create.vue b/resources/scripts/admin/views/users/Create.vue index e909601f..3219bf8a 100644 --- a/resources/scripts/admin/views/users/Create.vue +++ b/resources/scripts/admin/views/users/Create.vue @@ -128,6 +128,29 @@ + + + +