diff --git a/backend/app/Http/Actions/EmailTemplates/BaseEmailTemplateAction.php b/backend/app/Http/Actions/EmailTemplates/BaseEmailTemplateAction.php index d1af220eff..c8648519fa 100644 --- a/backend/app/Http/Actions/EmailTemplates/BaseEmailTemplateAction.php +++ b/backend/app/Http/Actions/EmailTemplates/BaseEmailTemplateAction.php @@ -3,15 +3,48 @@ namespace HiEvents\Http\Actions\EmailTemplates; use HiEvents\DomainObjects\Enums\EmailTemplateType; +use HiEvents\Exceptions\AccountNotVerifiedException; use HiEvents\Http\Actions\BaseAction; +use HiEvents\Repository\Interfaces\AccountRepositoryInterface; use HiEvents\Services\Application\Handlers\EmailTemplate\DTO\PreviewEmailTemplateDTO; use HiEvents\Services\Application\Handlers\EmailTemplate\PreviewEmailTemplateHandler; +use Illuminate\Config\Repository; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\Rules\Enum; abstract class BaseEmailTemplateAction extends BaseAction { + /** + * Yes, it's ugly to put this here, but it's in response to an ongoing spam issue. + * + * @throws AccountNotVerifiedException + */ + protected function verifyAccountCanModifyEmailTemplates(): void + { + /** @var Repository $config */ + $config = app(Repository::class); + + if (!$config->get('app.saas_mode_enabled')) { + return; + } + + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + + $account = $accountRepository->findById($this->getAuthenticatedAccountId()); + + if ($account->getAccountVerifiedAt() === null) { + throw new AccountNotVerifiedException(__('You cannot modify email templates until your account is verified.')); + } + + if (!$account->getIsManuallyVerified()) { + throw new AccountNotVerifiedException( + __('Due to issues with spam, you must connect a Stripe account before you can modify email templates.') + ); + } + } + protected function validateEmailTemplateRequest(Request $request): array { return $request->validate([ diff --git a/backend/app/Http/Actions/EmailTemplates/CreateEventEmailTemplateAction.php b/backend/app/Http/Actions/EmailTemplates/CreateEventEmailTemplateAction.php index 623628d9d2..8b52507ede 100644 --- a/backend/app/Http/Actions/EmailTemplates/CreateEventEmailTemplateAction.php +++ b/backend/app/Http/Actions/EmailTemplates/CreateEventEmailTemplateAction.php @@ -4,6 +4,7 @@ use HiEvents\DomainObjects\Enums\EmailTemplateType; use HiEvents\DomainObjects\EventDomainObject; +use HiEvents\Exceptions\AccountNotVerifiedException; use HiEvents\Exceptions\EmailTemplateValidationException; use HiEvents\Exceptions\ResourceConflictException; use HiEvents\Http\Resources\EmailTemplateResource; @@ -13,6 +14,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; +use Symfony\Component\HttpFoundation\Response; class CreateEventEmailTemplateAction extends BaseEmailTemplateAction { @@ -29,6 +31,12 @@ public function __invoke(Request $request, int $eventId): JsonResponse { $this->isActionAuthorized($eventId, EventDomainObject::class); + try { + $this->verifyAccountCanModifyEmailTemplates(); + } catch (AccountNotVerifiedException $e) { + return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED); + } + $validated = $this->validateEmailTemplateRequest($request); try { diff --git a/backend/app/Http/Actions/EmailTemplates/CreateOrganizerEmailTemplateAction.php b/backend/app/Http/Actions/EmailTemplates/CreateOrganizerEmailTemplateAction.php index cd89b0de05..c0e4411182 100644 --- a/backend/app/Http/Actions/EmailTemplates/CreateOrganizerEmailTemplateAction.php +++ b/backend/app/Http/Actions/EmailTemplates/CreateOrganizerEmailTemplateAction.php @@ -4,6 +4,7 @@ use HiEvents\DomainObjects\Enums\EmailTemplateType; use HiEvents\DomainObjects\OrganizerDomainObject; +use HiEvents\Exceptions\AccountNotVerifiedException; use HiEvents\Exceptions\EmailTemplateValidationException; use HiEvents\Exceptions\ResourceConflictException; use HiEvents\Http\Resources\EmailTemplateResource; @@ -13,6 +14,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; +use Symfony\Component\HttpFoundation\Response; class CreateOrganizerEmailTemplateAction extends BaseEmailTemplateAction { @@ -29,6 +31,12 @@ public function __invoke(Request $request, int $organizerId): JsonResponse { $this->isActionAuthorized($organizerId, OrganizerDomainObject::class); + try { + $this->verifyAccountCanModifyEmailTemplates(); + } catch (AccountNotVerifiedException $e) { + return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED); + } + $validated = $this->validateEmailTemplateRequest($request); try { diff --git a/backend/app/Http/Actions/EmailTemplates/DeleteEventEmailTemplateAction.php b/backend/app/Http/Actions/EmailTemplates/DeleteEventEmailTemplateAction.php index ed9a42b7dc..0051049cfd 100644 --- a/backend/app/Http/Actions/EmailTemplates/DeleteEventEmailTemplateAction.php +++ b/backend/app/Http/Actions/EmailTemplates/DeleteEventEmailTemplateAction.php @@ -3,15 +3,16 @@ namespace HiEvents\Http\Actions\EmailTemplates; use HiEvents\DomainObjects\EventDomainObject; +use HiEvents\Exceptions\AccountNotVerifiedException; use HiEvents\Exceptions\EmailTemplateNotFoundException; -use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\ResponseCodes; use HiEvents\Services\Application\Handlers\EmailTemplate\DeleteEmailTemplateHandler; use HiEvents\Services\Application\Handlers\EmailTemplate\DTO\DeleteEmailTemplateDTO; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; -class DeleteEventEmailTemplateAction extends BaseAction +class DeleteEventEmailTemplateAction extends BaseEmailTemplateAction { public function __construct( private readonly DeleteEmailTemplateHandler $handler @@ -23,6 +24,12 @@ public function __invoke(int $eventId, int $templateId): Response|JsonResponse { $this->isActionAuthorized($eventId, EventDomainObject::class); + try { + $this->verifyAccountCanModifyEmailTemplates(); + } catch (AccountNotVerifiedException $e) { + return $this->errorResponse($e->getMessage(), SymfonyResponse::HTTP_UNAUTHORIZED); + } + try { $this->handler->handle( new DeleteEmailTemplateDTO( diff --git a/backend/app/Http/Actions/EmailTemplates/DeleteOrganizerEmailTemplateAction.php b/backend/app/Http/Actions/EmailTemplates/DeleteOrganizerEmailTemplateAction.php index e24b17236c..d47d82f14b 100644 --- a/backend/app/Http/Actions/EmailTemplates/DeleteOrganizerEmailTemplateAction.php +++ b/backend/app/Http/Actions/EmailTemplates/DeleteOrganizerEmailTemplateAction.php @@ -3,14 +3,15 @@ namespace HiEvents\Http\Actions\EmailTemplates; use HiEvents\DomainObjects\OrganizerDomainObject; +use HiEvents\Exceptions\AccountNotVerifiedException; use HiEvents\Exceptions\EmailTemplateNotFoundException; -use HiEvents\Http\Actions\BaseAction; use HiEvents\Http\ResponseCodes; use HiEvents\Services\Application\Handlers\EmailTemplate\DeleteEmailTemplateHandler; use HiEvents\Services\Application\Handlers\EmailTemplate\DTO\DeleteEmailTemplateDTO; use Illuminate\Http\JsonResponse; +use Symfony\Component\HttpFoundation\Response; -class DeleteOrganizerEmailTemplateAction extends BaseAction +class DeleteOrganizerEmailTemplateAction extends BaseEmailTemplateAction { public function __construct( private readonly DeleteEmailTemplateHandler $handler @@ -21,6 +22,12 @@ public function __invoke(int $organizerId, int $templateId): JsonResponse { $this->isActionAuthorized($organizerId, OrganizerDomainObject::class); + try { + $this->verifyAccountCanModifyEmailTemplates(); + } catch (AccountNotVerifiedException $e) { + return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED); + } + try { $this->handler->handle( new DeleteEmailTemplateDTO( diff --git a/backend/app/Http/Actions/EmailTemplates/UpdateEventEmailTemplateAction.php b/backend/app/Http/Actions/EmailTemplates/UpdateEventEmailTemplateAction.php index 908f879345..f7be0096e3 100644 --- a/backend/app/Http/Actions/EmailTemplates/UpdateEventEmailTemplateAction.php +++ b/backend/app/Http/Actions/EmailTemplates/UpdateEventEmailTemplateAction.php @@ -4,6 +4,7 @@ use HiEvents\DomainObjects\Enums\EmailTemplateType; use HiEvents\DomainObjects\EventDomainObject; +use HiEvents\Exceptions\AccountNotVerifiedException; use HiEvents\Exceptions\EmailTemplateNotFoundException; use HiEvents\Exceptions\EmailTemplateValidationException; use HiEvents\Exceptions\InvalidEmailTemplateException; @@ -14,6 +15,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; +use Symfony\Component\HttpFoundation\Response; class UpdateEventEmailTemplateAction extends BaseEmailTemplateAction { @@ -30,6 +32,12 @@ public function __invoke(Request $request, int $eventId, int $templateId): JsonR { $this->isActionAuthorized($eventId, EventDomainObject::class); + try { + $this->verifyAccountCanModifyEmailTemplates(); + } catch (AccountNotVerifiedException $e) { + return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED); + } + $validated = $this->validateUpdateEmailTemplateRequest($request); try { diff --git a/backend/app/Http/Actions/EmailTemplates/UpdateOrganizerEmailTemplateAction.php b/backend/app/Http/Actions/EmailTemplates/UpdateOrganizerEmailTemplateAction.php index 8d767fa568..13620b45a8 100644 --- a/backend/app/Http/Actions/EmailTemplates/UpdateOrganizerEmailTemplateAction.php +++ b/backend/app/Http/Actions/EmailTemplates/UpdateOrganizerEmailTemplateAction.php @@ -4,6 +4,7 @@ use HiEvents\DomainObjects\Enums\EmailTemplateType; use HiEvents\DomainObjects\OrganizerDomainObject; +use HiEvents\Exceptions\AccountNotVerifiedException; use HiEvents\Exceptions\EmailTemplateNotFoundException; use HiEvents\Exceptions\EmailTemplateValidationException; use HiEvents\Exceptions\InvalidEmailTemplateException; @@ -14,6 +15,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; +use Symfony\Component\HttpFoundation\Response; class UpdateOrganizerEmailTemplateAction extends BaseEmailTemplateAction { @@ -29,6 +31,12 @@ public function __invoke(Request $request, int $organizerId, int $templateId): J { $this->isActionAuthorized($organizerId, OrganizerDomainObject::class); + try { + $this->verifyAccountCanModifyEmailTemplates(); + } catch (AccountNotVerifiedException $e) { + return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED); + } + $validated = $this->validateUpdateEmailTemplateRequest($request); try { diff --git a/frontend/src/components/common/EmailTemplateSettings/EmailTemplateSettingsBase.tsx b/frontend/src/components/common/EmailTemplateSettings/EmailTemplateSettingsBase.tsx index 8d472433af..dbc0fb44c7 100644 --- a/frontend/src/components/common/EmailTemplateSettings/EmailTemplateSettingsBase.tsx +++ b/frontend/src/components/common/EmailTemplateSettings/EmailTemplateSettingsBase.tsx @@ -1,6 +1,6 @@ import {useState} from 'react'; import {ActionIcon, Alert, Badge, Button, Group, LoadingOverlay, Modal, Paper, Stack, Text} from '@mantine/core'; -import {IconEdit, IconInfoCircle, IconMail, IconPlus, IconTrash} from '@tabler/icons-react'; +import {IconAlertCircle, IconEdit, IconInfoCircle, IconMail, IconPlus, IconTrash} from '@tabler/icons-react'; import {t, Trans} from '@lingui/macro'; import {useDisclosure} from '@mantine/hooks'; import {EmailTemplateEditor} from '../EmailTemplateEditor'; @@ -16,6 +16,8 @@ import { } from '../../../types'; import {Card} from '../Card'; import {HeadingWithDescription} from '../Card/CardHeading'; +import {useGetAccount} from '../../../queries/useGetAccount'; +import {StripeConnectButton} from '../StripeConnectButton'; interface EmailTemplateSettingsBaseProps { // Context @@ -72,6 +74,10 @@ export const EmailTemplateSettingsBase = ({ const [editingTemplate, setEditingTemplate] = useState(null); const [editingType, setEditingType] = useState('order_confirmation'); const handleFormError = useFormErrorResponseHandler(); + const {data: account, isFetched: isAccountFetched} = useGetAccount(); + const isAccountVerified = isAccountFetched && account?.is_account_email_confirmed; + const accountRequiresManualVerification = isAccountFetched && account?.requires_manual_verification; + const isModifyDisabled = !isAccountVerified || accountRequiresManualVerification; const orderConfirmationTemplate = templates.find(t => t.template_type === 'order_confirmation'); const attendeeTicketTemplate = templates.find(t => t.template_type === 'attendee_ticket'); @@ -271,6 +277,7 @@ export const EmailTemplateSettingsBase = ({ handleEditTemplate(template)} + disabled={isModifyDisabled} > @@ -279,6 +286,7 @@ export const EmailTemplateSettingsBase = ({ color="red" onClick={() => handleDeleteTemplate(template)} loading={deleteMutation.isPending} + disabled={isModifyDisabled} > @@ -288,6 +296,7 @@ export const EmailTemplateSettingsBase = ({ size="xs" leftSection={} onClick={() => handleCreateTemplate(type)} + disabled={isModifyDisabled} > Create Custom Template @@ -328,6 +337,25 @@ export const EmailTemplateSettingsBase = ({ description={getHeadingDescription()} /> + {(!isAccountVerified && isAccountFetched) && ( + } variant="light" mb="lg" color="orange"> + + {t`You need to verify your account email before you can modify email templates.`} + + + )} + + {accountRequiresManualVerification && ( + } variant="light" mb="lg" color="orange" title={t`Connect Stripe to enable email template editing`}> + + {t`Due to the high risk of spam, you must connect a Stripe account before you can modify email templates. This is to ensure that all event organizers are verified and accountable.`} + +
+ +
+
+ )} + } variant="light" mb="lg"> {getAlertMessage()}