Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/cypress/src/utils/supabaseTestsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ export class SupabaseTestsService {
continue;
}

// Log errors so they're visible in CI
console.error(`[${this.tenantId}] Error seeding ${table}:`, {
error: result.error,
message: result.error?.message,
code: result.error?.code,
details: result.error?.details,
hint: result.error?.hint,
rowCount: rows.length
});

results[table] = result;
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/User/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const MESSAGE_MIN_CHARACTERS = 10;
export const MESSAGE_MIN_CHARACTERS = 20;
export const MESSAGE_MAX_CHARACTERS = 500;
4 changes: 2 additions & 2 deletions src/pages/User/contact/UserContactFieldMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FieldTextarea } from 'oa-components';
import { Field } from 'react-final-form';
import { MESSAGE_MAX_CHARACTERS, MESSAGE_MIN_CHARACTERS } from 'src/pages/User/constants';
import { contact } from 'src/pages/User/labels';
import { required } from 'src/utils/validators';
import { minValue, required } from 'src/utils/validators';
import { Flex, Label } from 'theme-ui';

export const UserContactFieldMessage = () => {
Expand All @@ -21,7 +21,7 @@ export const UserContactFieldMessage = () => {
modifiers={{ capitalize: true, trim: true }}
component={FieldTextarea}
sx={{ backgroundColor: 'white' }}
validate={required}
validate={(value) => required(value) || minValue(MESSAGE_MIN_CHARACTERS)(value)}
validateFields={[]}
showCharacterCount
/>
Expand Down
2 changes: 2 additions & 0 deletions src/pages/User/contact/UserContactFieldName.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FieldInput } from 'oa-components';
import { Field } from 'react-final-form';
import { contact } from 'src/pages/User/labels';
import { required } from 'src/utils/validators';
import { Flex, Label } from 'theme-ui';

export const UserContactFieldName = () => {
Expand All @@ -17,6 +18,7 @@ export const UserContactFieldName = () => {
name={name}
placeholder={placeholder}
sx={{ backgroundColor: 'white' }}
validate={required}
validateFields={[]}
/>
</Flex>
Expand Down
28 changes: 13 additions & 15 deletions src/pages/User/contact/UserContactForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,31 @@ export const UserContactForm = observer(({ user }: Props) => {

const [submitResults, setSubmitResults] = useState<SubmitResults | null>(null);

const { button, title, successMessage } = contact;
const buttonName = 'contact-submit';
const formId = 'contact-form';

const onSubmit = async (formValues, form) => {
setSubmitResults(null);
const response = await messageService.sendMessage({
to: user.username,
message: formValues.message,
name: formValues.name,
});
try {
await messageService.sendMessage({
to: user.username,
message: formValues.message,
name: formValues.name,
});

if (response.ok) {
form.restart();
return setSubmitResults({ type: 'success', message: successMessage });
return setSubmitResults({ type: 'success', message: contact.successMessage });
} catch (error) {
if (error.message) {
setSubmitResults({ type: 'error', message: error.message });
}
}

return setSubmitResults({
type: 'error',
message: `${response.statusText}. Please try again or report the problem.`,
});
};

return (
<Flex sx={{ flexDirection: 'column' }} data-cy="UserContactForm">
<Heading as="h3" variant="small" mb={2}>
{`${title} ${user.displayName}`}
{`${contact.title} ${user.displayName}`}
</Heading>
<Form
onSubmit={onSubmit}
Expand All @@ -76,7 +74,7 @@ export const UserContactForm = observer(({ user }: Props) => {
disabled={submitting}
form={formId}
>
{button}
{contact.button}
</Button>
</Box>
</Flex>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/User/labels.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MESSAGE_MAX_CHARACTERS } from './constants';

export const contact = {
button: 'Contact',
button: 'Send Message',
email: {
title: 'Email (currently fixed to your email on record)',
placeholder: 'hey@jack.com',
Expand Down
64 changes: 35 additions & 29 deletions src/routes/api.messages.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { HTTPException } from 'hono/http-exception';
import type { ActionFunctionArgs } from 'react-router';
import { MESSAGE_MAX_CHARACTERS, MESSAGE_MIN_CHARACTERS } from 'src/pages/User/constants';
import { createSupabaseServerClient } from 'src/repository/supabase.server';
import { TenantSettingsService } from 'src/services/tenantSettingsService.server';
import {
methodNotAllowedError,
tooManyRequestsError,
unauthorizedError,
validationError,
} from 'src/utils/httpException';
import { sendEmail } from '../.server/resend';
import ReceiverMessage from '../.server/templates/ReceiverMessage';

Expand All @@ -19,18 +27,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const claims = await client.auth.getClaims();

if (!claims.data?.claims) {
return Response.json({}, { headers, status: 401 });
throw unauthorizedError();
}

const { valid, status, statusText } = await validateRequest(
request,
claims.data.claims.email!,
data,
);

if (!valid) {
return Response.json({}, { headers, status, statusText });
}
await validateRequest(request, claims.data.claims.email!, data);

const userProfile = await client
.from('profiles')
Expand Down Expand Up @@ -59,14 +59,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}

if (countResult.count! >= 20) {
return Response.json(
{ error: 'Too many requests' },
{
headers,
status: 429,
statusText:
"You've contacted a lot of people today! So to protect the platform from spam we haven't sent this message.",
},
throw tooManyRequestsError(
"You've contacted a lot of people today! So to protect the platform from spam we haven't sent this message.",
);
}

Expand Down Expand Up @@ -116,37 +110,49 @@ export const action = async ({ request }: ActionFunctionArgs) => {
});

if (sendResult.error) {
return Response.json(
{ error: sendResult.error },
{ headers, status: 429, statusText: sendResult.error },
);
throw sendResult.error;
}

return Response.json(null, { headers, status: 201 });
} catch (error) {
if (error instanceof HTTPException) {
return error.getResponse();
}

console.error(error);

return Response.json({ error }, { headers, status: 500, statusText: 'Error sending message' });
}
};

async function validateRequest(request: Request, userEmail: string | null, data: any) {
// TODO: Create a MessageDTO
if (request.method !== 'POST') {
return { status: 405, statusText: 'method not allowed' };
throw methodNotAllowedError();
}

if (!data.to) {
return { status: 400, statusText: 'to is required' };
throw validationError('to is required', 'to');
}

if (!data.message) {
return { status: 400, statusText: 'message is required' };
throw validationError('message is required', 'message');
}

if (!userEmail) {
return { status: 400, statusText: 'Unable to get messenger email address' };
if (data.message.length < MESSAGE_MIN_CHARACTERS) {
throw validationError(
`Message must be at least ${MESSAGE_MIN_CHARACTERS} characters`,
'message',
);
}

if (data.message.length > MESSAGE_MAX_CHARACTERS) {
throw validationError(
`Message must be no more than ${MESSAGE_MAX_CHARACTERS} characters`,
'message',
);
}

return { valid: true };
if (!userEmail) {
throw validationError('Unable to get messenger email address', 'email');
}
}
3 changes: 2 additions & 1 deletion src/routes/api.news.$id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
forbiddenError,
methodNotAllowedError,
notFoundError,
unauthorizedError,
validationError,
} from 'src/utils/httpException';
import { convertToSlug } from 'src/utils/slug';
Expand All @@ -39,7 +40,7 @@ export const action = async ({ request, params }: LoaderFunctionArgs) => {
const claims = await client.auth.getClaims();

if (!claims.data?.claims) {
return Response.json({}, { headers, status: 401 });
throw unauthorizedError();
}

const currentNews = await new NewsServiceServer(client).getById(id);
Expand Down
12 changes: 10 additions & 2 deletions src/services/messageService.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import type { SendMessage } from 'oa-shared';

const sendMessage = async (data: SendMessage) => {
const sendMessage = async (data: SendMessage): Promise<void> => {
const formData = new FormData();

formData.append('to', data.to);
formData.append('message', data.message);
formData.append('name', data.name);

return fetch('/api/messages', {
const response = await fetch('/api/messages', {
method: 'POST',
body: formData,
});

if (response.status !== 200 && response.status !== 201) {
const errorData = await response.json().catch(() => ({ error: 'Error saving research' }));
const errorMessage = errorData.error || errorData.message || 'Error saving research';
throw new Error(errorMessage, { cause: response.status });
}

return;
};

export const messageService = {
Expand Down
4 changes: 4 additions & 0 deletions src/utils/httpException.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ export function forbiddenError(message = 'Forbidden') {
export function conflictError(message: string) {
return createHTTPException(409, message);
}

export function tooManyRequestsError(message: string) {
return createHTTPException(429, message);
}
Loading