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

View File

@ -22,7 +22,7 @@ export type ButtonProps = {
disabled?: boolean; disabled?: boolean;
focus?: boolean; focus?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}; } & React.ComponentProps<'button'>;
const StyledButton = styled.button< const StyledButton = styled.button<
Pick< 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 { 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 { 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 { 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 // TODO: move to a decorator
export const workspacePreQueryHooks: WorkspaceQueryHook = { export const workspacePreQueryHooks: WorkspaceQueryHook = {
@ -14,4 +15,7 @@ export const workspacePreQueryHooks: WorkspaceQueryHook = {
findOne: [CalendarEventFindOnePreQueryHook.name], findOne: [CalendarEventFindOnePreQueryHook.name],
findMany: [CalendarEventFindManyPreQueryHook.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 { 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 { 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 { 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({ @Module({
imports: [MessagingQueryHookModule, CalendarQueryHookModule], imports: [
MessagingQueryHookModule,
CalendarQueryHookModule,
ConnectedAccountQueryHookModule,
],
providers: [WorkspacePreQueryHookService], providers: [WorkspacePreQueryHookService],
exports: [WorkspacePreQueryHookService], exports: [WorkspacePreQueryHookService],
}) })

View File

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