Public: anyone can read this; the crew can change it.

Sign-up's username and taken-email sentences live in one module, read by every way an account is made #5608

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/5592-sign-up-s-username-and-taken-email-sente → main 2 commits fork 1 thread · 1 unresolved

  • Merged. mason/5592-sign-up-s-username-and-taken-email-sente was merged into main as 77acca0.

Review

Sign in to review this change.
7 files changed +52 −7
src/actions/first_admin.ts +3 −2
@@ -30,6 +30,7 @@ import { bucketOf } from "../http/peer.ts";
3030 import { err, ok, type Result } from "../kernel/result.ts";
3131 import { limitValue } from "../settings/limits.ts";
3232 import { handOver, spawnedOrg } from "../spawn/claim.ts";
33 import { EMAIL_TAKEN_MESSAGE, INVALID_USERNAME_MESSAGE } from "./signup_words.ts";
3334
3435 const USERNAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,38}$/;
3536
@@ -48,10 +49,10 @@ export const FIRST_ADMIN_WORDS: Record<FirstAdminError, string> = {
4849 rate_limited: "Too many attempts from here. Wait a minute and try again.",
4950 refused:
5051 "That first-admin code does not work: it may be mistyped, used, expired or replaced, or this forge already has an admin. Ask the operator to run --first-admin again.",
51 invalid_username: "Usernames are letters, digits, dots and dashes.",
52 invalid_username: INVALID_USERNAME_MESSAGE,
5253 invalid_email: "An email address, please.",
5354 username_taken: "That username is taken.",
54 email_taken: "That email already has an account.",
55 email_taken: EMAIL_TAKEN_MESSAGE,
5556 username_reserved: "That name is reserved.",
5657 unfinished: "The account could not be finished. Try again.",
5758 };
src/actions/signup_words.ts added +12
@@ -0,0 +1,12 @@
1 /**
2 * The sentences every way of making an account says when a username is
3 * malformed or an email is already held: the sign-up form, the passkey
4 * sign-up, a claim, an everyday invite and the first-admin door. One place,
5 * so they read the same wherever they are said.
6 */
7
8 /** A username outside the safe-name alphabet. */
9 export const INVALID_USERNAME_MESSAGE = "Usernames are letters, digits, dots and dashes.";
10
11 /** An email that already belongs to an account. */
12 export const EMAIL_TAKEN_MESSAGE = "That email already has an account.";
src/web/pages/claim.ts +2 −1
@@ -58,6 +58,7 @@ import { passkeyJs } from "../passkey-client.ts";
5858 import { passkeyNote, passkeyState } from "../passkey-state.ts";
5959 import { btn, card, field as formField } from "../ui.ts";
6060 import { sessionCookie, signinAllowed } from "./site.ts";
61 import { EMAIL_TAKEN_MESSAGE } from "../../actions/signup_words.ts";

Nit: the import sits after the ../ui and ./site imports, out of the file order of the other imports. Put it with the other ../../actions imports.

6162
6263 const USERNAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,38}$/;
6364
@@ -345,7 +346,7 @@ export async function claimSubmit(ctx: Ctx): Promise<Response> {
345346 unburnClaim(ctx.app.db, orgId, token);
346347 return again(
347348 made.error === "email_taken"
348 ? { email: "That email already has an account." }
349 ? { email: EMAIL_TAKEN_MESSAGE }
349350 : made.error === "username_reserved"
350351 ? { username: "That name is reserved." }
351352 : { username: "That username is taken." },
src/web/pages/everyday.ts +2 −1
@@ -33,6 +33,7 @@ import { passkeyNote } from "../passkey-state.ts";
3333 import { btn, card, field as formField } from "../ui.ts";
3434 import { signOutHere } from "../../actions/signins.ts";
3535 import { sessionCookie } from "./site.ts";
36 import { EMAIL_TAKEN_MESSAGE } from "../../actions/signup_words.ts";
3637
3738 const USERNAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,38}$/;
3839
@@ -193,7 +194,7 @@ export async function everydayCreate(ctx: Ctx): Promise<Response> {
193194 made.error === "username_taken"
194195 ? "That username is taken."
195196 : made.error === "email_taken"
196 ? "That email already has an account."
197 ? EMAIL_TAKEN_MESSAGE
197198 : made.error === "username_reserved"
198199 ? "That name is taken by the forge itself; pick another."
199200 : "That account could not be made.",
src/web/pages/signup-passkey.ts +3 −2
@@ -17,6 +17,7 @@ import { clientAddress, clientBucket } from "../../http/peer.ts";
1717 import { boundedBody, json, str } from "../../http/respond.ts";
1818 import { relying, store } from "./passkeys.ts";
1919 import { EDGE_NAME_MESSAGE, inviteOf, pinnedTo, safeNext, sessionCookie, signinAllowed, signupSlot } from "./site.ts";
20 import { EMAIL_TAKEN_MESSAGE, INVALID_USERNAME_MESSAGE } from "../../actions/signup_words.ts";
2021 import { PASSKEY_EXPIRED, PASSKEY_REJECTED, PASSKEY_TAKEN } from "../passkey-words.ts";
2122
2223 const refuse = (status: number, message: string) =>
@@ -24,10 +25,10 @@ const refuse = (status: number, message: string) =>
2425
2526 /** The words for each way a sign-up can fail, written for anyone. */
2627 const WORDS: Record<string, string> = {
27 invalid_username: "Usernames are letters, digits, dots and dashes.",
28 invalid_username: INVALID_USERNAME_MESSAGE,
2829 username_taken: "That username is taken.",
2930 username_reserved: "That name is reserved.",
30 email_taken: "That email already has an account.",
31 email_taken: EMAIL_TAKEN_MESSAGE,
3132 invalid_email: "That does not look like an email address.",
3233 edge_name: EDGE_NAME_MESSAGE,
3334 human_check: HUMAN_CHECK_MESSAGE,
src/web/pages/site.ts +2 −1
@@ -54,6 +54,7 @@ import { existsSync } from "node:fs";
5454 import { join } from "node:path";
5555 import { hasAdmin } from "../../spawn/claim.ts";
5656 import { getSignup, type SignupMode } from "../../settings/signup.ts";
57 import { EMAIL_TAKEN_MESSAGE } from "../../actions/signup_words.ts";
5758
5859 export function sessionCookie(
5960 value: string,
@@ -662,7 +663,7 @@ export async function signupSubmit(ctx: Ctx): Promise<Response> {
662663 },
663664 username_taken: { username: "That username is taken." },
664665 username_reserved: { username: "That name is reserved." },
665 email_taken: { email: "That email already has an account." },
666 email_taken: { email: EMAIL_TAKEN_MESSAGE },
666667 password_too_long: { password: PASSWORD_TOO_LONG },
667668 weak_password: { password: "Passwords need at least 12 characters." },
668669 invalid_email: { email: "That does not look like an email address." },
test/signup-words.test.ts added +28
@@ -0,0 +1,28 @@
1 import { describe, expect, test } from "bun:test";
2 import { readFileSync } from "node:fs";
3 import { join } from "node:path";
4 import { EMAIL_TAKEN_MESSAGE, INVALID_USERNAME_MESSAGE } from "../src/actions/signup_words.ts";
5
6 const SENTENCES = { INVALID_USERNAME_MESSAGE, EMAIL_TAKEN_MESSAGE };
7
8 const READERS = [
9 "src/actions/first_admin.ts",
10 "src/web/pages/claim.ts",
11 "src/web/pages/everyday.ts",
12 "src/web/pages/signup-passkey.ts",
13 "src/web/pages/site.ts",
14 ];
15
16 describe("signup words", () => {
17 for (const [name, sentence] of Object.entries(SENTENCES))
18 test(`${name} is a sentence`, () => {
19 expect(sentence.trim().length).toBeGreaterThan(0);
20 });
21
22 for (const file of READERS)
23 test(`${file} reads the sentences rather than typing them`, () => {
24 const source = readFileSync(join(import.meta.dir, "..", file), "utf8");
25 for (const sentence of Object.values(SENTENCES)) expect(source).not.toContain(sentence);
26 expect(source).toContain("signup_words.ts");
27 });
28 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs