4001 add validation for blocklist (#5172)

Closes #4001
This commit is contained in:
bosiraphael 2024-04-25 15:32:55 +02:00 committed by GitHub
parent 4af2c5f298
commit d23e02adca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 148 additions and 32 deletions

View File

@ -1,9 +1,13 @@
import { useState } from 'react';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { Key } from 'ts-key-enum';
import { z } from 'zod';
import { Button } from '@/ui/input/button/components/Button';
import { TextInput } from '@/ui/input/components/TextInput';
import { isDomain } from '~/utils/is-domain';
const StyledContainer = styled.div`
display: flex;
@ -19,45 +23,75 @@ type SettingsAccountsEmailsBlocklistInputProps = {
updateBlockedEmailList: (email: string) => void;
};
const validationSchema = z
.object({
emailOrDomain: z
.string()
.trim()
.email('Invalid email or domain')
.or(
z
.string()
.refine(
(value) => value.startsWith('@') && isDomain(value.slice(1)),
'Invalid email or domain',
),
),
})
.required();
type FormInput = z.infer<typeof validationSchema>;
export const SettingsAccountsEmailsBlocklistInput = ({
updateBlockedEmailList,
}: SettingsAccountsEmailsBlocklistInputProps) => {
const [formValues, setFormValues] = useState<{
email: string;
}>({
email: '',
const { reset, handleSubmit, control, formState } = useForm<FormInput>({
mode: 'onSubmit',
resolver: zodResolver(validationSchema),
defaultValues: {
emailOrDomain: '',
},
});
const submit = handleSubmit((data) => {
updateBlockedEmailList(data.emailOrDomain);
});
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === Key.Enter) {
updateBlockedEmailList(formValues.email);
setFormValues({ email: '' });
submit();
}
};
const { isSubmitSuccessful } = formState;
useEffect(() => {
if (isSubmitSuccessful) {
reset();
}
}, [isSubmitSuccessful, reset]);
return (
<StyledContainer>
<StyledLinkContainer>
<TextInput
placeholder="eddy@gmail.com, @apple.com"
value={formValues.email}
onChange={(value) => {
setFormValues((prevState) => ({
...prevState,
email: value,
}));
}}
fullWidth
onKeyDown={handleKeyDown}
/>
</StyledLinkContainer>
<Button
title="Add to blocklist"
onClick={() => {
updateBlockedEmailList(formValues.email);
setFormValues({ email: '' });
}}
/>
</StyledContainer>
<form onSubmit={submit}>
<StyledContainer>
<StyledLinkContainer>
<Controller
name="emailOrDomain"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<TextInput
placeholder="eddy@gmail.com, @apple.com"
value={value}
onChange={onChange}
error={error?.message}
onKeyDown={handleKeyDown}
fullWidth
/>
)}
/>
</StyledLinkContainer>
<Button title="Add to blocklist" type="submit" />
</StyledContainer>
</form>
);
};

View File

@ -22,7 +22,7 @@ export type ButtonProps = {
disabled?: boolean;
focus?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
} & React.ComponentProps<'button'>;
const StyledButton = styled.button<
Pick<

View File

@ -3,6 +3,7 @@ import { MessageFindOnePreQueryHook } from 'src/modules/messaging/query-hooks/me
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type';
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
import { BlocklistCreateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook';
// TODO: move to a decorator
export const workspacePreQueryHooks: WorkspaceQueryHook = {
@ -14,4 +15,7 @@ export const workspacePreQueryHooks: WorkspaceQueryHook = {
findOne: [CalendarEventFindOnePreQueryHook.name],
findMany: [CalendarEventFindManyPreQueryHook.name],
},
blocklist: {
createMany: [BlocklistCreateManyPreQueryHook.name],
},
};

View File

@ -3,9 +3,14 @@ import { Module } from '@nestjs/common';
import { MessagingQueryHookModule } from 'src/modules/messaging/query-hooks/messaging-query-hook.module';
import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module';
import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module';
@Module({
imports: [MessagingQueryHookModule, CalendarQueryHookModule],
imports: [
MessagingQueryHookModule,
CalendarQueryHookModule,
ConnectedAccountQueryHookModule,
],
providers: [WorkspacePreQueryHookService],
exports: [WorkspacePreQueryHookService],
})

View File

@ -227,6 +227,14 @@ export class WorkspaceQueryRunnerService {
options,
);
await this.workspacePreQueryHookService.executePreHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
'createMany',
args,
);
const query = await this.workspaceQueryBuilderFactory.createMany(
computedArgs,
options,

View File

@ -0,0 +1,5 @@
export const isDomain = (url: string | undefined | null) =>
!!url &&
/^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/.test(
url,
);

View File

@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import z from 'zod';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { isDomain } from 'src/engine/utils/is-domain';
import { BlocklistObjectMetadata } from 'src/modules/connected-account/standard-objects/blocklist.object-metadata';
@Injectable()
export class BlocklistCreateManyPreQueryHook implements WorkspacePreQueryHook {
constructor() {}
async execute(
userId: string,
workspaceId: string,
payload: CreateManyResolverArgs<
Omit<BlocklistObjectMetadata, 'createdAt' | 'updatedAt'> & {
createdAt: string;
updatedAt: string;
}
>,
): Promise<void> {
const emailOrDomainSchema = z
.string()
.trim()
.email('Invalid email or domain')
.or(
z
.string()
.refine(
(value) => value.startsWith('@') && isDomain(value.slice(1)),
'Invalid email or domain',
),
);
for (const { handle } of payload.data) {
if (!handle) {
throw new Error('Handle is required');
}
emailOrDomainSchema.parse(handle);
}
}
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { BlocklistCreateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook';
@Module({
imports: [],
providers: [
{
provide: BlocklistCreateManyPreQueryHook.name,
useClass: BlocklistCreateManyPreQueryHook,
},
],
})
export class ConnectedAccountQueryHookModule {}