Skip to content

Commit 95feb8f

Browse files
authored
Feature: Disable event templates for unverified users (#948)
1 parent 04f12c8 commit 95feb8f

File tree

8 files changed

+112
-5
lines changed

8 files changed

+112
-5
lines changed

backend/app/Http/Actions/EmailTemplates/BaseEmailTemplateAction.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,48 @@
33
namespace HiEvents\Http\Actions\EmailTemplates;
44

55
use HiEvents\DomainObjects\Enums\EmailTemplateType;
6+
use HiEvents\Exceptions\AccountNotVerifiedException;
67
use HiEvents\Http\Actions\BaseAction;
8+
use HiEvents\Repository\Interfaces\AccountRepositoryInterface;
79
use HiEvents\Services\Application\Handlers\EmailTemplate\DTO\PreviewEmailTemplateDTO;
810
use HiEvents\Services\Application\Handlers\EmailTemplate\PreviewEmailTemplateHandler;
11+
use Illuminate\Config\Repository;
912
use Illuminate\Http\JsonResponse;
1013
use Illuminate\Http\Request;
1114
use Illuminate\Validation\Rules\Enum;
1215

1316
abstract class BaseEmailTemplateAction extends BaseAction
1417
{
18+
/**
19+
* Yes, it's ugly to put this here, but it's in response to an ongoing spam issue.
20+
*
21+
* @throws AccountNotVerifiedException
22+
*/
23+
protected function verifyAccountCanModifyEmailTemplates(): void
24+
{
25+
/** @var Repository $config */
26+
$config = app(Repository::class);
27+
28+
if (!$config->get('app.saas_mode_enabled')) {
29+
return;
30+
}
31+
32+
/** @var AccountRepositoryInterface $accountRepository */
33+
$accountRepository = app(AccountRepositoryInterface::class);
34+
35+
$account = $accountRepository->findById($this->getAuthenticatedAccountId());
36+
37+
if ($account->getAccountVerifiedAt() === null) {
38+
throw new AccountNotVerifiedException(__('You cannot modify email templates until your account is verified.'));
39+
}
40+
41+
if (!$account->getIsManuallyVerified()) {
42+
throw new AccountNotVerifiedException(
43+
__('Due to issues with spam, you must connect a Stripe account before you can modify email templates.')
44+
);
45+
}
46+
}
47+
1548
protected function validateEmailTemplateRequest(Request $request): array
1649
{
1750
return $request->validate([

backend/app/Http/Actions/EmailTemplates/CreateEventEmailTemplateAction.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use HiEvents\DomainObjects\Enums\EmailTemplateType;
66
use HiEvents\DomainObjects\EventDomainObject;
7+
use HiEvents\Exceptions\AccountNotVerifiedException;
78
use HiEvents\Exceptions\EmailTemplateValidationException;
89
use HiEvents\Exceptions\ResourceConflictException;
910
use HiEvents\Http\Resources\EmailTemplateResource;
@@ -13,6 +14,7 @@
1314
use Illuminate\Http\JsonResponse;
1415
use Illuminate\Http\Request;
1516
use Illuminate\Validation\ValidationException;
17+
use Symfony\Component\HttpFoundation\Response;
1618

1719
class CreateEventEmailTemplateAction extends BaseEmailTemplateAction
1820
{
@@ -29,6 +31,12 @@ public function __invoke(Request $request, int $eventId): JsonResponse
2931
{
3032
$this->isActionAuthorized($eventId, EventDomainObject::class);
3133

34+
try {
35+
$this->verifyAccountCanModifyEmailTemplates();
36+
} catch (AccountNotVerifiedException $e) {
37+
return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED);
38+
}
39+
3240
$validated = $this->validateEmailTemplateRequest($request);
3341

3442
try {

backend/app/Http/Actions/EmailTemplates/CreateOrganizerEmailTemplateAction.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use HiEvents\DomainObjects\Enums\EmailTemplateType;
66
use HiEvents\DomainObjects\OrganizerDomainObject;
7+
use HiEvents\Exceptions\AccountNotVerifiedException;
78
use HiEvents\Exceptions\EmailTemplateValidationException;
89
use HiEvents\Exceptions\ResourceConflictException;
910
use HiEvents\Http\Resources\EmailTemplateResource;
@@ -13,6 +14,7 @@
1314
use Illuminate\Http\JsonResponse;
1415
use Illuminate\Http\Request;
1516
use Illuminate\Validation\ValidationException;
17+
use Symfony\Component\HttpFoundation\Response;
1618

1719
class CreateOrganizerEmailTemplateAction extends BaseEmailTemplateAction
1820
{
@@ -29,6 +31,12 @@ public function __invoke(Request $request, int $organizerId): JsonResponse
2931
{
3032
$this->isActionAuthorized($organizerId, OrganizerDomainObject::class);
3133

34+
try {
35+
$this->verifyAccountCanModifyEmailTemplates();
36+
} catch (AccountNotVerifiedException $e) {
37+
return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED);
38+
}
39+
3240
$validated = $this->validateEmailTemplateRequest($request);
3341

3442
try {

backend/app/Http/Actions/EmailTemplates/DeleteEventEmailTemplateAction.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
namespace HiEvents\Http\Actions\EmailTemplates;
44

55
use HiEvents\DomainObjects\EventDomainObject;
6+
use HiEvents\Exceptions\AccountNotVerifiedException;
67
use HiEvents\Exceptions\EmailTemplateNotFoundException;
7-
use HiEvents\Http\Actions\BaseAction;
88
use HiEvents\Http\ResponseCodes;
99
use HiEvents\Services\Application\Handlers\EmailTemplate\DeleteEmailTemplateHandler;
1010
use HiEvents\Services\Application\Handlers\EmailTemplate\DTO\DeleteEmailTemplateDTO;
1111
use Illuminate\Http\JsonResponse;
1212
use Illuminate\Http\Response;
13+
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
1314

14-
class DeleteEventEmailTemplateAction extends BaseAction
15+
class DeleteEventEmailTemplateAction extends BaseEmailTemplateAction
1516
{
1617
public function __construct(
1718
private readonly DeleteEmailTemplateHandler $handler
@@ -23,6 +24,12 @@ public function __invoke(int $eventId, int $templateId): Response|JsonResponse
2324
{
2425
$this->isActionAuthorized($eventId, EventDomainObject::class);
2526

27+
try {
28+
$this->verifyAccountCanModifyEmailTemplates();
29+
} catch (AccountNotVerifiedException $e) {
30+
return $this->errorResponse($e->getMessage(), SymfonyResponse::HTTP_UNAUTHORIZED);
31+
}
32+
2633
try {
2734
$this->handler->handle(
2835
new DeleteEmailTemplateDTO(

backend/app/Http/Actions/EmailTemplates/DeleteOrganizerEmailTemplateAction.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
namespace HiEvents\Http\Actions\EmailTemplates;
44

55
use HiEvents\DomainObjects\OrganizerDomainObject;
6+
use HiEvents\Exceptions\AccountNotVerifiedException;
67
use HiEvents\Exceptions\EmailTemplateNotFoundException;
7-
use HiEvents\Http\Actions\BaseAction;
88
use HiEvents\Http\ResponseCodes;
99
use HiEvents\Services\Application\Handlers\EmailTemplate\DeleteEmailTemplateHandler;
1010
use HiEvents\Services\Application\Handlers\EmailTemplate\DTO\DeleteEmailTemplateDTO;
1111
use Illuminate\Http\JsonResponse;
12+
use Symfony\Component\HttpFoundation\Response;
1213

13-
class DeleteOrganizerEmailTemplateAction extends BaseAction
14+
class DeleteOrganizerEmailTemplateAction extends BaseEmailTemplateAction
1415
{
1516
public function __construct(
1617
private readonly DeleteEmailTemplateHandler $handler
@@ -21,6 +22,12 @@ public function __invoke(int $organizerId, int $templateId): JsonResponse
2122
{
2223
$this->isActionAuthorized($organizerId, OrganizerDomainObject::class);
2324

25+
try {
26+
$this->verifyAccountCanModifyEmailTemplates();
27+
} catch (AccountNotVerifiedException $e) {
28+
return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED);
29+
}
30+
2431
try {
2532
$this->handler->handle(
2633
new DeleteEmailTemplateDTO(

backend/app/Http/Actions/EmailTemplates/UpdateEventEmailTemplateAction.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use HiEvents\DomainObjects\Enums\EmailTemplateType;
66
use HiEvents\DomainObjects\EventDomainObject;
7+
use HiEvents\Exceptions\AccountNotVerifiedException;
78
use HiEvents\Exceptions\EmailTemplateNotFoundException;
89
use HiEvents\Exceptions\EmailTemplateValidationException;
910
use HiEvents\Exceptions\InvalidEmailTemplateException;
@@ -14,6 +15,7 @@
1415
use Illuminate\Http\JsonResponse;
1516
use Illuminate\Http\Request;
1617
use Illuminate\Validation\ValidationException;
18+
use Symfony\Component\HttpFoundation\Response;
1719

1820
class UpdateEventEmailTemplateAction extends BaseEmailTemplateAction
1921
{
@@ -30,6 +32,12 @@ public function __invoke(Request $request, int $eventId, int $templateId): JsonR
3032
{
3133
$this->isActionAuthorized($eventId, EventDomainObject::class);
3234

35+
try {
36+
$this->verifyAccountCanModifyEmailTemplates();
37+
} catch (AccountNotVerifiedException $e) {
38+
return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED);
39+
}
40+
3341
$validated = $this->validateUpdateEmailTemplateRequest($request);
3442

3543
try {

backend/app/Http/Actions/EmailTemplates/UpdateOrganizerEmailTemplateAction.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use HiEvents\DomainObjects\Enums\EmailTemplateType;
66
use HiEvents\DomainObjects\OrganizerDomainObject;
7+
use HiEvents\Exceptions\AccountNotVerifiedException;
78
use HiEvents\Exceptions\EmailTemplateNotFoundException;
89
use HiEvents\Exceptions\EmailTemplateValidationException;
910
use HiEvents\Exceptions\InvalidEmailTemplateException;
@@ -14,6 +15,7 @@
1415
use Illuminate\Http\JsonResponse;
1516
use Illuminate\Http\Request;
1617
use Illuminate\Validation\ValidationException;
18+
use Symfony\Component\HttpFoundation\Response;
1719

1820
class UpdateOrganizerEmailTemplateAction extends BaseEmailTemplateAction
1921
{
@@ -29,6 +31,12 @@ public function __invoke(Request $request, int $organizerId, int $templateId): J
2931
{
3032
$this->isActionAuthorized($organizerId, OrganizerDomainObject::class);
3133

34+
try {
35+
$this->verifyAccountCanModifyEmailTemplates();
36+
} catch (AccountNotVerifiedException $e) {
37+
return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED);
38+
}
39+
3240
$validated = $this->validateUpdateEmailTemplateRequest($request);
3341

3442
try {

frontend/src/components/common/EmailTemplateSettings/EmailTemplateSettingsBase.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {useState} from 'react';
22
import {ActionIcon, Alert, Badge, Button, Group, LoadingOverlay, Modal, Paper, Stack, Text} from '@mantine/core';
3-
import {IconEdit, IconInfoCircle, IconMail, IconPlus, IconTrash} from '@tabler/icons-react';
3+
import {IconAlertCircle, IconEdit, IconInfoCircle, IconMail, IconPlus, IconTrash} from '@tabler/icons-react';
44
import {t, Trans} from '@lingui/macro';
55
import {useDisclosure} from '@mantine/hooks';
66
import {EmailTemplateEditor} from '../EmailTemplateEditor';
@@ -16,6 +16,8 @@ import {
1616
} from '../../../types';
1717
import {Card} from '../Card';
1818
import {HeadingWithDescription} from '../Card/CardHeading';
19+
import {useGetAccount} from '../../../queries/useGetAccount';
20+
import {StripeConnectButton} from '../StripeConnectButton';
1921

2022
interface EmailTemplateSettingsBaseProps {
2123
// Context
@@ -72,6 +74,10 @@ export const EmailTemplateSettingsBase = ({
7274
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
7375
const [editingType, setEditingType] = useState<EmailTemplateType>('order_confirmation');
7476
const handleFormError = useFormErrorResponseHandler();
77+
const {data: account, isFetched: isAccountFetched} = useGetAccount();
78+
const isAccountVerified = isAccountFetched && account?.is_account_email_confirmed;
79+
const accountRequiresManualVerification = isAccountFetched && account?.requires_manual_verification;
80+
const isModifyDisabled = !isAccountVerified || accountRequiresManualVerification;
7581

7682
const orderConfirmationTemplate = templates.find(t => t.template_type === 'order_confirmation');
7783
const attendeeTicketTemplate = templates.find(t => t.template_type === 'attendee_ticket');
@@ -271,6 +277,7 @@ export const EmailTemplateSettingsBase = ({
271277
<ActionIcon
272278
variant="subtle"
273279
onClick={() => handleEditTemplate(template)}
280+
disabled={isModifyDisabled}
274281
>
275282
<IconEdit size={16}/>
276283
</ActionIcon>
@@ -279,6 +286,7 @@ export const EmailTemplateSettingsBase = ({
279286
color="red"
280287
onClick={() => handleDeleteTemplate(template)}
281288
loading={deleteMutation.isPending}
289+
disabled={isModifyDisabled}
282290
>
283291
<IconTrash size={16}/>
284292
</ActionIcon>
@@ -288,6 +296,7 @@ export const EmailTemplateSettingsBase = ({
288296
size="xs"
289297
leftSection={<IconPlus size={16}/>}
290298
onClick={() => handleCreateTemplate(type)}
299+
disabled={isModifyDisabled}
291300
>
292301
<Trans>Create Custom Template</Trans>
293302
</Button>
@@ -328,6 +337,25 @@ export const EmailTemplateSettingsBase = ({
328337
description={getHeadingDescription()}
329338
/>
330339

340+
{(!isAccountVerified && isAccountFetched) && (
341+
<Alert icon={<IconAlertCircle size={16}/>} variant="light" mb="lg" color="orange">
342+
<Text size="sm">
343+
{t`You need to verify your account email before you can modify email templates.`}
344+
</Text>
345+
</Alert>
346+
)}
347+
348+
{accountRequiresManualVerification && (
349+
<Alert icon={<IconAlertCircle size={16}/>} variant="light" mb="lg" color="orange" title={t`Connect Stripe to enable email template editing`}>
350+
<Text size="sm">
351+
{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.`}
352+
</Text>
353+
<div style={{marginTop: '0.75rem'}}>
354+
<StripeConnectButton/>
355+
</div>
356+
</Alert>
357+
)}
358+
331359
<Alert icon={<IconInfoCircle size={16}/>} variant="light" mb="lg">
332360
<Text size="sm">
333361
{getAlertMessage()}

0 commit comments

Comments
 (0)