Skip to content

Commit 0bdea25

Browse files
authored
refactor(forms-api): deepen public form intake (#86)
* refactor(forms-api): deepen public form intake Centralize shared public form intake ordering while keeping each form's spam policy explicit. * chore: remove context doc from forms intake PR
1 parent abacae1 commit 0bdea25

3 files changed

Lines changed: 169 additions & 178 deletions

File tree

Lines changed: 30 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { ERROR_IDS } from "../../constants/error-ids.js";
2-
import { HttpError } from "../../shared-api/errors/http-error.js";
32
import type { EmailServiceInterface } from "../../shared-api/services/email/index.js";
43
import type { Logger } from "../../shared-api/types/logger.js";
54
import type { FormResponse } from "../schemas/form-response-schemas.js";
65
import { buildContactFormNotification } from "../services/build-contact-form-notification.js";
76
import type { FormStorageService } from "../services/form-storage/interface.js";
87
import type { ContactFormData } from "../services/form-storage/types.js";
98
import type { RecaptchaService } from "../services/recaptcha/interface.js";
10-
import { runSpamChecks } from "../utils/run-spam-checks.js";
11-
import { sendAndPersist } from "../utils/send-and-persist.js";
9+
import { processPublicFormIntake } from "../utils/process-public-form-intake.js";
1210

1311
export async function handleContactFormLogic({
1412
formData,
@@ -33,95 +31,35 @@ export async function handleContactFormLogic({
3331
logger: Logger;
3432
set: { status?: number | string };
3533
}): Promise<FormResponse> {
36-
try {
37-
const verification = await recaptchaService.verifyToken({
38-
token: recaptchaToken,
39-
secretKey: recaptchaSecretKey,
40-
logger,
41-
});
42-
43-
if (!verification.success) {
44-
logger.warn("reCAPTCHA verification failed for contact form", {
45-
errorId: ERROR_IDS.RECAPTCHA_VERIFICATION_FAILED,
46-
error: verification.error,
47-
});
48-
set.status = 400;
49-
return { success: false, error: "reCAPTCHA verification failed" };
50-
}
51-
52-
const rejection = runSpamChecks({
53-
policy: {
54-
recaptcha: { score: verification.score },
55-
honeypot: { value: honeypotValue },
56-
gibberish: [
57-
{ fieldName: "contactName", text: formData.contactName },
58-
{ fieldName: "message", text: formData.message },
59-
],
60-
timing: { formLoadedAt, minMillis: 3000 },
61-
},
62-
submitterEmail: formData.email,
63-
submitterName: formData.contactName,
34+
return processPublicFormIntake({
35+
recaptchaToken,
36+
recaptchaSecretKey,
37+
recaptchaService,
38+
emailService,
39+
logger,
40+
set,
41+
formContext: {
6442
formType: "contact form",
43+
formTypeKey: "contact_form",
6544
errorId: ERROR_IDS.CONTACT_FORM_PROCESSING_FAILED,
66-
logger,
67-
set,
68-
});
69-
if (rejection !== undefined) {
70-
return rejection;
71-
}
72-
73-
const { emailSent, warning } = await sendAndPersist({
74-
buildEmail: () => buildContactFormNotification(formData),
75-
persist: ({ emailSent: sent }) =>
76-
formStorageService.saveContactForm({
77-
data: formData,
78-
recaptchaScore: verification.score,
79-
emailSent: sent,
80-
}),
81-
emailService,
82-
logger,
83-
formContext: {
84-
formType: "contact form",
85-
formTypeKey: "contact_form",
86-
errorId: ERROR_IDS.CONTACT_FORM_PROCESSING_FAILED,
87-
submitterEmail: formData.email,
88-
submitterName: formData.contactName,
89-
recaptchaScore: verification.score,
90-
},
91-
});
92-
93-
logger.info("Contact form submitted successfully", {
94-
email: formData.email,
95-
recaptchaScore: verification.score,
96-
emailSent,
97-
});
98-
99-
set.status = 200;
100-
const result: FormResponse = {
101-
success: true,
102-
message: "Form submitted successfully",
103-
emailSent,
104-
};
105-
106-
if (warning) {
107-
result.warning = warning;
108-
}
109-
110-
return result;
111-
} catch (error) {
112-
if (error instanceof HttpError) {
113-
set.status = error.statusCode;
114-
return { success: false, error: error.message };
115-
}
116-
117-
logger.error("Failed to process contact form", {
118-
errorId: ERROR_IDS.CONTACT_FORM_PROCESSING_FAILED,
119-
error,
120-
errorMessage: error instanceof Error ? error.message : "Unknown error",
121-
errorStack: error instanceof Error ? error.stack : undefined,
122-
});
123-
124-
set.status = 500;
125-
return { success: false, error: "Internal server error" };
126-
}
45+
submitterEmail: formData.email,
46+
submitterName: formData.contactName,
47+
},
48+
getSpamPolicy: recaptchaScore => ({
49+
recaptcha: { score: recaptchaScore },
50+
honeypot: { value: honeypotValue },
51+
gibberish: [
52+
{ fieldName: "contactName", text: formData.contactName },
53+
{ fieldName: "message", text: formData.message },
54+
],
55+
timing: { formLoadedAt, minMillis: 3000 },
56+
}),
57+
buildEmail: () => buildContactFormNotification(formData),
58+
persist: ({ emailSent, recaptchaScore }) =>
59+
formStorageService.saveContactForm({
60+
data: formData,
61+
recaptchaScore,
62+
emailSent,
63+
}),
64+
});
12765
}
Lines changed: 24 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { ERROR_IDS } from "../../constants/error-ids.js";
2-
import { HttpError } from "../../shared-api/errors/http-error.js";
32
import type { EmailServiceInterface } from "../../shared-api/services/email/index.js";
43
import type { Logger } from "../../shared-api/types/logger.js";
54
import type { FormResponse } from "../schemas/form-response-schemas.js";
65
import { buildDoulaMatchNotification } from "../services/build-doula-match-notification.js";
76
import type { FormStorageService } from "../services/form-storage/interface.js";
87
import type { DoulaMatchData } from "../services/form-storage/types.js";
98
import type { RecaptchaService } from "../services/recaptcha/interface.js";
10-
import { runSpamChecks } from "../utils/run-spam-checks.js";
11-
import { sendAndPersist } from "../utils/send-and-persist.js";
9+
import { processPublicFormIntake } from "../utils/process-public-form-intake.js";
1210

1311
export async function handleMatchFormLogic({
1412
formData,
@@ -29,89 +27,29 @@ export async function handleMatchFormLogic({
2927
logger: Logger;
3028
set: { status?: number | string };
3129
}): Promise<FormResponse> {
32-
try {
33-
const verification = await recaptchaService.verifyToken({
34-
token: recaptchaToken,
35-
secretKey: recaptchaSecretKey,
36-
logger,
37-
});
38-
39-
if (!verification.success) {
40-
logger.warn("reCAPTCHA verification failed for doula match form", {
41-
errorId: ERROR_IDS.RECAPTCHA_VERIFICATION_FAILED,
42-
error: verification.error,
43-
});
44-
set.status = 400;
45-
return { success: false, error: "reCAPTCHA verification failed" };
46-
}
47-
48-
const rejection = runSpamChecks({
49-
policy: {
50-
recaptcha: { score: verification.score },
51-
},
52-
submitterEmail: formData.email,
53-
submitterName: formData.name,
30+
return processPublicFormIntake({
31+
recaptchaToken,
32+
recaptchaSecretKey,
33+
recaptchaService,
34+
emailService,
35+
logger,
36+
set,
37+
formContext: {
5438
formType: "doula match form",
39+
formTypeKey: "doula_match",
5540
errorId: ERROR_IDS.DOULA_MATCH_FORM_PROCESSING_FAILED,
56-
logger,
57-
set,
58-
});
59-
if (rejection !== undefined) {
60-
return rejection;
61-
}
62-
63-
const { emailSent, warning } = await sendAndPersist({
64-
buildEmail: () => buildDoulaMatchNotification(formData),
65-
persist: ({ emailSent: sent }) =>
66-
formStorageService.saveMatchRequest({
67-
data: formData,
68-
recaptchaScore: verification.score,
69-
emailSent: sent,
70-
}),
71-
emailService,
72-
logger,
73-
formContext: {
74-
formType: "doula match form",
75-
formTypeKey: "doula_match",
76-
errorId: ERROR_IDS.DOULA_MATCH_FORM_PROCESSING_FAILED,
77-
submitterEmail: formData.email,
78-
submitterName: formData.name,
79-
recaptchaScore: verification.score,
80-
},
81-
});
82-
83-
logger.info("Doula match form submitted successfully", {
84-
email: formData.email,
85-
recaptchaScore: verification.score,
86-
emailSent,
87-
});
88-
89-
set.status = 200;
90-
const result: FormResponse = {
91-
success: true,
92-
message: "Form submitted successfully",
93-
emailSent,
94-
};
95-
96-
if (warning) {
97-
result.warning = warning;
98-
}
99-
100-
return result;
101-
} catch (error) {
102-
if (error instanceof HttpError) {
103-
set.status = error.statusCode;
104-
return { success: false, error: error.message };
105-
}
106-
107-
logger.error("Failed to process doula match form", {
108-
errorId: ERROR_IDS.DOULA_MATCH_FORM_PROCESSING_FAILED,
109-
error,
110-
errorMessage: error instanceof Error ? error.message : "Unknown error",
111-
errorStack: error instanceof Error ? error.stack : undefined,
112-
});
113-
114-
set.status = 500;
115-
return { success: false, error: "Internal server error" };
116-
}
41+
submitterEmail: formData.email,
42+
submitterName: formData.name,
43+
},
44+
getSpamPolicy: recaptchaScore => ({
45+
recaptcha: { score: recaptchaScore },
46+
}),
47+
buildEmail: () => buildDoulaMatchNotification(formData),
48+
persist: ({ emailSent, recaptchaScore }) =>
49+
formStorageService.saveMatchRequest({
50+
data: formData,
51+
recaptchaScore,
52+
emailSent,
53+
}),
54+
});
11755
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { ERROR_IDS } from "../../constants/error-ids.js";
2+
import { HttpError } from "../../shared-api/errors/http-error.js";
3+
import type { EmailServiceInterface } from "../../shared-api/services/email/index.js";
4+
import type { EmailMessage } from "../../shared-api/services/email/types.js";
5+
import type { Logger } from "../../shared-api/types/logger.js";
6+
import type { FormResponse } from "../schemas/form-response-schemas.js";
7+
import type { RecaptchaService } from "../services/recaptcha/interface.js";
8+
import { runSpamChecks, type SpamPolicy } from "./run-spam-checks.js";
9+
import { sendAndPersist } from "./send-and-persist.js";
10+
11+
export async function processPublicFormIntake({
12+
recaptchaToken,
13+
recaptchaSecretKey,
14+
recaptchaService,
15+
emailService,
16+
logger,
17+
set,
18+
formContext,
19+
getSpamPolicy,
20+
buildEmail,
21+
persist,
22+
}: {
23+
recaptchaToken: string;
24+
recaptchaSecretKey: string;
25+
recaptchaService: RecaptchaService;
26+
emailService: EmailServiceInterface;
27+
logger: Logger;
28+
set: { status?: number | string };
29+
formContext: {
30+
formType: string;
31+
formTypeKey: string;
32+
errorId: string;
33+
submitterEmail: string;
34+
submitterName: string;
35+
};
36+
getSpamPolicy: (recaptchaScore: number) => SpamPolicy;
37+
buildEmail: () => EmailMessage;
38+
persist: (options: { emailSent: boolean; recaptchaScore: number }) => Promise<void>;
39+
}): Promise<FormResponse> {
40+
try {
41+
const verification = await recaptchaService.verifyToken({
42+
token: recaptchaToken,
43+
secretKey: recaptchaSecretKey,
44+
logger,
45+
});
46+
47+
if (!verification.success) {
48+
logger.warn(`reCAPTCHA verification failed for ${formContext.formType}`, {
49+
errorId: ERROR_IDS.RECAPTCHA_VERIFICATION_FAILED,
50+
error: verification.error,
51+
});
52+
set.status = 400;
53+
return { success: false, error: "reCAPTCHA verification failed" };
54+
}
55+
56+
const rejection = runSpamChecks({
57+
policy: getSpamPolicy(verification.score),
58+
submitterEmail: formContext.submitterEmail,
59+
submitterName: formContext.submitterName,
60+
formType: formContext.formType,
61+
errorId: formContext.errorId,
62+
logger,
63+
set,
64+
});
65+
if (rejection !== undefined) {
66+
return rejection;
67+
}
68+
69+
const { emailSent, warning } = await sendAndPersist({
70+
buildEmail,
71+
persist: ({ emailSent: sent }) =>
72+
persist({ emailSent: sent, recaptchaScore: verification.score }),
73+
emailService,
74+
logger,
75+
formContext: {
76+
...formContext,
77+
recaptchaScore: verification.score,
78+
},
79+
});
80+
81+
logger.info(`${formContext.formType} submitted successfully`, {
82+
email: formContext.submitterEmail,
83+
recaptchaScore: verification.score,
84+
emailSent,
85+
});
86+
87+
set.status = 200;
88+
const result: FormResponse = {
89+
success: true,
90+
message: "Form submitted successfully",
91+
emailSent,
92+
};
93+
94+
if (warning) {
95+
result.warning = warning;
96+
}
97+
98+
return result;
99+
} catch (error) {
100+
if (error instanceof HttpError) {
101+
set.status = error.statusCode;
102+
return { success: false, error: error.message };
103+
}
104+
105+
logger.error(`Failed to process ${formContext.formType}`, {
106+
errorId: formContext.errorId,
107+
error,
108+
errorMessage: error instanceof Error ? error.message : "Unknown error",
109+
errorStack: error instanceof Error ? error.stack : undefined,
110+
});
111+
112+
set.status = 500;
113+
return { success: false, error: "Internal server error" };
114+
}
115+
}

0 commit comments

Comments
 (0)