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
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rssamplifier/web",
"version": "0.2.1",
"version": "0.2.2",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -11,7 +11,7 @@
},
"dependencies": {
"@logicsrc/openaccess": "^0.3.1",
"@profullstack/contact-form": "0.2.0",
"@profullstack/contact-form": "0.3.0",
"@profullstack/footer": "latest",
"@profullstack/leaderboard": "^0.3.1",
"@profullstack/notifications": "^0.1.4",
Expand Down
24 changes: 3 additions & 21 deletions apps/web/src/app/sales/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { corpusFigures } from '../../lib/corpus.js';
import { latestClosedWindow } from '../../lib/datasetWindow.js';
import Toolbar from '../Toolbar.jsx';
import { ContactForm } from '@profullstack/contact-form/react';
import { formErrors, salesContact } from '../../lib/salesContact.js';
import { salesContact } from '../../lib/salesContact.js';

export const metadata = {
title: 'Training data',
Expand Down Expand Up @@ -203,7 +203,8 @@ export default async function SalesPage({ searchParams }) {
) : null}
{error ? (
<p className="notice" role="alert">
{errorMessage(error)}
{/* Rebuilt from the codes a plain form post comes back with; the URL never carries text. */}
{salesContact.errorMessage(params)}
</p>
) : null}
<p>
Expand Down Expand Up @@ -233,25 +234,6 @@ export default async function SalesPage({ searchParams }) {
);
}

/**
* @param {string} code
* @returns {string}
*/
function errorMessage(code) {
// A message from the form's own checks, sent back by a plain form post.
if (formErrors().has(code)) return code;
switch (code) {
case 'bad-email':
return 'That does not look like an email address — we would have no way to reply.';
case 'no-use-case':
return 'Please say what you want the corpus for. It is the field that decides what we can offer.';
case 'too-many':
return 'That is several enquiries in an hour. Email hello@rssamplifier.com directly and we will pick it up there.';
default:
return 'That did not send. Try again, or email hello@rssamplifier.com.';
}
}

/**
* @param {unknown} n
* @returns {string}
Expand Down
29 changes: 4 additions & 25 deletions apps/web/src/lib/salesContact.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export const MAX_PER_WINDOW = 3;

export const TOO_MANY =
'That is several enquiries in an hour. Email hello@rssamplifier.com directly and we will pick it up there.';
export const NO_USE_CASE =
'Please say what you want the corpus for — it is the field that decides what we can offer.';
/** What any failure the sender cannot fix says, instead of the error itself. */
export const DID_NOT_SEND = 'That did not send. Try again, or email hello@rssamplifier.com.';

/** The address a request comes from, the way every other limit on this site reads it. */
const ipOf = (req) => clientIp({ headers: req.headers }) || null;
Expand Down Expand Up @@ -111,8 +111,7 @@ export function makeSalesContact(deps = {}) {
rows: 5,
maxLength: 4000,
required: true,
pattern: '^[\\s\\S]{10,}$',
patternMessage: NO_USE_CASE,
minLength: 10,
help: 'The last box is the one that decides what we can offer, so it is the one worth writing. Rough is fine; vague costs us both a round trip.',
},
],
Expand Down Expand Up @@ -153,30 +152,10 @@ export function makeSalesContact(deps = {}) {
'Thank you — that is recorded and someone will reply to the address you gave.',
responses: {
limited: { body: { ok: false, error: TOO_MANY }, status: 429 },
error: { body: { ok: false, error: DID_NOT_SEND }, status: 500 },
},
});
}

export const salesContact = makeSalesContact();

/**
* Every message a plain form post (JavaScript off) can be sent back to /sales
* with in `?error=`. The page shows one of these or a generic line, never
* whatever text a link happens to carry.
*/
export function formErrors(form = salesContact) {
const out = new Set([
TOO_MANY,
'That took too long. Please send it again.',
'Too many messages. Please try again later.',
'Invalid submission',
]);
for (const q of form.questions) {
if (q.type === 'section') continue;
out.add(`${q.label} is required`);
out.add(`${q.label} is too long (${q.maxLength} characters at most)`);
if (q.type === 'email') out.add(`${q.label} must be a valid email address`);
if (q.patternMessage) out.add(q.patternMessage);
}
return out;
}
24 changes: 20 additions & 4 deletions apps/web/test/sales-contact.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test } from 'node:test';

import { createFormGuard } from '@profullstack/contact-form';

import { MAX_PER_WINDOW, NO_USE_CASE, TOO_MANY, formErrors, makeSalesContact } from '../src/lib/salesContact.js';
import { MAX_PER_WINDOW, TOO_MANY, makeSalesContact } from '../src/lib/salesContact.js';

const guard = createFormGuard({ secret: 'test-secret-not-used-anywhere-else', binding: 'rssamplifier:sales' });

Expand Down Expand Up @@ -92,11 +92,14 @@ test('a JSON caller may send useCase', async () => {
assert.equal(rows[0].useCase, use_case);
});

test('a use case under ten characters is refused with the old wording', async () => {
test('a use case under ten characters is refused', async () => {
const { form, rows } = harness();
const res = await form.POST(json({ ...answers, use_case: 'data', fg_token: await token() }));
assert.equal(res.status, 400);
assert.deepEqual(await res.json(), { ok: false, error: NO_USE_CASE });
assert.deepEqual(await res.json(), {
ok: false,
error: 'What do you want the corpus for? is too short (10 characters at least)',
});
assert.equal(rows.length, 0);
});

Expand All @@ -121,5 +124,18 @@ test('a plain form post is sent back to the form, with a message the page will s
const location = new URL(res.headers.get('location'), 'http://localhost');
assert.equal(location.pathname, '/sales');
assert.equal(location.hash, '#enquire');
assert.ok(formErrors(form).has(location.searchParams.get('error')));
// Codes, never text: the page rebuilds the message from them.
assert.equal(location.searchParams.get('error'), 'invalid');
assert.equal(location.searchParams.get('field'), 'use_case');
assert.equal(
form.errorMessage(location.searchParams),
'What do you want the corpus for? is too short (10 characters at least)',
);
});

test('a crafted ?error= shows the form\'s own wording, never the link\'s text', () => {
const { form } = harness();
assert.equal(form.errorMessage({ error: 'Send your password to evil.example' }), form.errorMessage({ error: 'error' }));
assert.equal(form.errorMessage({ error: 'limited' }), TOO_MANY);
assert.equal(form.errorMessage({}), null);
});
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rssamplifier.com",
"version": "0.2.1",
"version": "0.2.2",
"private": true,
"type": "module",
"description": "Agent-first public RSS directory. Submit a URL, a list or an OPML file; every blog gets its own page.",
Expand Down
Loading