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

Taken and reserved names, and an empty email, are said from one module by every way an account or org is made #5715

Merged opened this

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

Merged

mason/forge:mason/5653-taken-and-reserved-names-and-an-empty-em → main 3 commits fork 1 thread · 1 unresolved

  • Merged. mason/5653-taken-and-reserved-names-and-an-empty-em was merged into main as cacab12.

Review

Sign in to review this change.
9 files changed +72 −25
src/actions/first_admin.ts +4 −4
@@ -30,7 +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";
33 import { EMAIL_TAKEN_MESSAGE, EMAIL_WANTED, INVALID_USERNAME_MESSAGE, NAME_RESERVED, USERNAME_TAKEN } from "./signup_words.ts";
3434
3535 const USERNAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,38}$/;
3636
@@ -50,10 +50,10 @@ export const FIRST_ADMIN_WORDS: Record<FirstAdminError, string> = {
5050 refused:
5151 "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.",
5252 invalid_username: INVALID_USERNAME_MESSAGE,
53 invalid_email: "An email address, please.",
54 username_taken: "That username is taken.",
53 invalid_email: EMAIL_WANTED,
54 username_taken: USERNAME_TAKEN,
5555 email_taken: EMAIL_TAKEN_MESSAGE,
56 username_reserved: "That name is reserved.",
56 username_reserved: NAME_RESERVED,
5757 unfinished: "The account could not be finished. Try again.",
5858 };
5959
src/actions/signup_words.ts +8 −4
@@ -1,8 +1,8 @@
11 /**
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.
2 * The sentences a person reads when the name or email they chose for a new
3 * account or org will not do. Every door that makes one (the sign-up and
4 * claim pages, an everyday invite, the first admin, the crew and org forms,
5 * passkey sign-up) says them from here, so they read the same everywhere.
66 */
77
88 /** A username outside the safe-name alphabet. */
@@ -10,3 +10,7 @@ export const INVALID_USERNAME_MESSAGE = "Usernames are letters, digits, dots and
1010
1111 /** An email that already belongs to an account. */
1212 export const EMAIL_TAKEN_MESSAGE = "That email already has an account.";
13
14 export const USERNAME_TAKEN = "That username is taken.";
15 export const NAME_RESERVED = "That name is reserved.";
16 export const EMAIL_WANTED = "An email address, please.";
src/web/pages/claim.ts +4 −4
@@ -58,7 +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";
61 import { EMAIL_TAKEN_MESSAGE, NAME_RESERVED, USERNAME_TAKEN } from "../../actions/signup_words.ts";
6262
6363 const USERNAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,38}$/;
6464
@@ -257,7 +257,7 @@ function problems(ctx: Ctx, username: string, email: string, needEmail: boolean)
257257 if (!USERNAME.test(username))
258258 errors.username = "Usernames are letters, digits, dots, dashes.";
259259 else if (getUserByName(ctx.app.db, username))
260 errors.username = "That username is taken.";
260 errors.username = USERNAME_TAKEN;
261261 if (email !== "" ? !givenEmail(email) : needEmail)
262262 errors.email = "That does not look like an email address.";
263263 return errors;
@@ -348,8 +348,8 @@ export async function claimSubmit(ctx: Ctx): Promise<Response> {
348348 made.error === "email_taken"
349349 ? { email: EMAIL_TAKEN_MESSAGE }
350350 : made.error === "username_reserved"
351 ? { username: "That name is reserved." }
352 : { username: "That username is taken." },
351 ? { username: NAME_RESERVED }
352 : { username: USERNAME_TAKEN },
353353 );
354354 }
355355 if (credential !== undefined && !addPasskey(ctx.app.db, made.value.id, "Passkey", credential).ok) {
src/web/pages/crew.ts +3 −2
@@ -61,6 +61,7 @@ import {
6161 import { accountSections } from "./origins.ts";
6262 import { markOf } from "../../core/marks.ts";
6363 import { limitValue } from "../../settings/limits.ts";
64 import { USERNAME_TAKEN } from "../../actions/signup_words.ts";
6465
6566 /** The admin, or the response that turns everyone else away. */
6667 function requireAdmin(ctx: Ctx): Response | undefined {
@@ -146,8 +147,8 @@ interface AgentValues {
146147
147148 const agentErrors = (ctx: Ctx): Record<string, string> => ({
148149 invalid_username: "Usernames are letters, digits, dots, dashes.",
149 username_taken: "That username is taken.",
150 email_taken: "That username is taken.",
150 username_taken: USERNAME_TAKEN,
151 email_taken: USERNAME_TAKEN,
151152 invalid_model: `A model is up to ${MODEL_MAX} characters with no spaces.`,
152153 brief_too_long: `A brief is at most ${BRIEF_MAX} characters.`,
153154 invalid_budget: `A budget is a number of dollars between 0 and ${limitValue(ctx.app.db, "agent.budget_max_usd")}.`,
src/web/pages/everyday.ts +4 −4
@@ -33,7 +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";
36 import { EMAIL_TAKEN_MESSAGE, EMAIL_WANTED, USERNAME_TAKEN } from "../../actions/signup_words.ts";
3737
3838 const USERNAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,38}$/;
3939
@@ -133,7 +133,7 @@ function everydayFields(ctx: Ctx, user: User, offerKeep: boolean): Html {
133133 /** Why `username` cannot be the everyday account's name, or undefined. */
134134 function nameProblem(ctx: Ctx, username: string): string | undefined {
135135 if (!USERNAME.test(username)) return "Usernames are letters, digits, dots, dashes.";
136 if (getUserByName(ctx.app.db, username)) return "That username is taken.";
136 if (getUserByName(ctx.app.db, username)) return USERNAME_TAKEN;
137137 return undefined;
138138 }
139139
@@ -152,7 +152,7 @@ export async function everydayOptions(ctx: Ctx): Promise<Response> {
152152 const data = await boundedBody(ctx.request, STRANGER_MAX);
153153 if (data === "too_large") return refuse(413, "payload too large");
154154 const username = str(data.username).trim();
155 const bad = nameProblem(ctx, username) ?? (str(data.email).includes("@") ? undefined : "An email address, please.");
155 const bad = nameProblem(ctx, username) ?? (str(data.email).includes("@") ? undefined : EMAIL_WANTED);
156156 if (bad !== undefined) return refuse(422, bad);
157157 return json(makeOptions(ctx, "everyday", username));
158158 }
@@ -192,7 +192,7 @@ export async function everydayCreate(ctx: Ctx): Promise<Response> {
192192 if (!made.ok)
193193 return say(
194194 made.error === "username_taken"
195 ? "That username is taken."
195 ? USERNAME_TAKEN
196196 : made.error === "email_taken"
197197 ? EMAIL_TAKEN_MESSAGE
198198 : made.error === "username_reserved"
src/web/pages/org.ts +2 −1
@@ -58,6 +58,7 @@ import {
5858 time,
5959 field as uiField,
6060 } from "../ui.ts";
61 import { NAME_RESERVED } from "../../actions/signup_words.ts";
6162
6263 const PER_PAGE = 20;
6364
@@ -459,7 +460,7 @@ export async function newOrgSubmit(ctx: Ctx): Promise<Response> {
459460 if (!created.ok) {
460461 const messages: Record<string, string> = {
461462 name_taken: "That name is already taken.",

The taken-name sentence on the line above is still typed here and differs from the account wording. Fold it into signup_words.ts.

462 name_reserved: "That name is reserved.",
463 name_reserved: NAME_RESERVED,
463464 invalid_name: "Letters, digits, dots and dashes only.",
464465 unauthorized: "Sign in to create an organization.",
465466 };
src/web/pages/signup-passkey.ts +3 −3
@@ -17,7 +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";
20 import { EMAIL_TAKEN_MESSAGE, INVALID_USERNAME_MESSAGE, NAME_RESERVED, USERNAME_TAKEN } from "../../actions/signup_words.ts";
2121 import { PASSKEY_EXPIRED, PASSKEY_REJECTED, PASSKEY_TAKEN } from "../passkey-words.ts";
2222
2323 const refuse = (status: number, message: string) =>
@@ -26,8 +26,8 @@ const refuse = (status: number, message: string) =>
2626 /** The words for each way a sign-up can fail, written for anyone. */
2727 const WORDS: Record<string, string> = {
2828 invalid_username: INVALID_USERNAME_MESSAGE,
29 username_taken: "That username is taken.",
30 username_reserved: "That name is reserved.",
29 username_taken: USERNAME_TAKEN,
30 username_reserved: NAME_RESERVED,
3131 email_taken: EMAIL_TAKEN_MESSAGE,
3232 invalid_email: "That does not look like an email address.",
3333 edge_name: EDGE_NAME_MESSAGE,
src/web/pages/site.ts +3 −3
@@ -54,7 +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";
57 import { EMAIL_TAKEN_MESSAGE, NAME_RESERVED, USERNAME_TAKEN } from "../../actions/signup_words.ts";
5858
5959 export function sessionCookie(
6060 value: string,
@@ -661,8 +661,8 @@ export async function signupSubmit(ctx: Ctx): Promise<Response> {
661661 invalid_username: {
662662 username: "Usernames are letters, digits, dots, dashes.",
663663 },
664 username_taken: { username: "That username is taken." },
665 username_reserved: { username: "That name is reserved." },
664 username_taken: { username: USERNAME_TAKEN },
665 username_reserved: { username: NAME_RESERVED },
666666 email_taken: { email: EMAIL_TAKEN_MESSAGE },
667667 password_too_long: { password: PASSWORD_TOO_LONG },
668668 weak_password: { password: "Passwords need at least 12 characters." },
test/name-words.test.ts added +41
@@ -0,0 +1,41 @@
1 /**
2 * A taken username, a reserved name and a missing email are said from one
3 * module, src/actions/signup_words.ts. Every door that makes an account or
4 * an org imports the sentence; none types its own copy, so a copy typed
5 * back fails here.
6 */
7
8 import { describe, expect, test } from "bun:test";
9 import { readFileSync } from "node:fs";
10 import { join } from "node:path";
11 import { EMAIL_WANTED, NAME_RESERVED, USERNAME_TAKEN } from "../src/actions/signup_words.ts";
12
13 const ROOT = join(import.meta.dir, "..");
14
15 const DOORS = [
16 "src/actions/first_admin.ts",
17 "src/web/pages/claim.ts",
18 "src/web/pages/crew.ts",
19 "src/web/pages/everyday.ts",
20 "src/web/pages/org.ts",
21 "src/web/pages/signup-passkey.ts",
22 "src/web/pages/site.ts",
23 ];
24
25 const SENTENCES = [USERNAME_TAKEN, NAME_RESERVED, EMAIL_WANTED];
26
27 describe("name words", () => {
28 test("the module says each sentence as it always read", () => {
29 expect(USERNAME_TAKEN).toBe("That username is taken.");
30 expect(NAME_RESERVED).toBe("That name is reserved.");
31 expect(EMAIL_WANTED).toBe("An email address, please.");
32 });
33
34 for (const file of DOORS) {
35 test(`${file} types none of the sentences itself`, () => {
36 const text = readFileSync(join(ROOT, file), "utf8");
37 for (const sentence of SENTENCES) expect(text).not.toContain(sentence);
38 expect(text).toContain("signup_words.ts");
39 });
40 }
41 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs