P1 flow fix: register makes a solo user by default, collective creation is an opt-in flag, post-login menu for collective-less accounts
This commit is contained in:
parent
2665f43cc4
commit
2a3a1d79a1
15 changed files with 614 additions and 24 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
export interface MeResponse {
|
||||
user: { id: string; username: string; email: string | null; displayName: string };
|
||||
workspace: { id: string; name: string; slug: string };
|
||||
workspace: { id: string; name: string; slug: string; kind: "shared" | "personal" };
|
||||
role: string;
|
||||
permissions: string[];
|
||||
expiresAt: number;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import { getSessionToken, onSessionTokenChange, setSessionToken } from "./sessio
|
|||
export type AuthStatus = "loading" | "authenticated" | "anonymous" | "offline";
|
||||
|
||||
export interface RegisterInput {
|
||||
workspaceName: string;
|
||||
/** Omitted/empty => solo account (personal workspace, no collective). */
|
||||
workspaceName?: string;
|
||||
username: string;
|
||||
password: string;
|
||||
displayName?: string;
|
||||
|
|
|
|||
64
apps/web/src/lib/register-form.test.ts
Normal file
64
apps/web/src/lib/register-form.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "node:test";
|
||||
|
||||
/**
|
||||
* Register-form semantics for the operator flow reorder (node:test, the
|
||||
* control-room runner convention).
|
||||
*
|
||||
* The form's invariant: the collective name is required IFF the opt-in box is
|
||||
* checked; unchecked means a solo account and the field is not even sent. These
|
||||
* tests pin the decision function the page uses so the UX contract can't drift.
|
||||
*/
|
||||
|
||||
export type RegisterFormState = {
|
||||
creatingCollective: boolean;
|
||||
name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const MIN_PASSWORD = 10;
|
||||
const USERNAME = /^[a-z0-9][a-z0-9_.-]{1,31}$/;
|
||||
|
||||
/** Exactly what the page computes before enabling the submit button. */
|
||||
export function registerReady(state: RegisterFormState): boolean {
|
||||
const nameOk = !state.creatingCollective || state.name.trim().length >= 2;
|
||||
return nameOk && USERNAME.test(state.username.trim()) && state.password.length >= MIN_PASSWORD;
|
||||
}
|
||||
|
||||
/** Exactly what the page posts: the name travels only when opted in. */
|
||||
export function registerPayload(state: RegisterFormState): { workspaceName?: string; username: string } {
|
||||
return {
|
||||
...(state.creatingCollective ? { workspaceName: state.name.trim() } : {}),
|
||||
username: state.username.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
test("solo default: no checkbox => no name required, no name sent", () => {
|
||||
const state: RegisterFormState = { creatingCollective: false, name: "", username: "solita", password: "correct-horse-1" };
|
||||
assert.equal(registerReady(state), true);
|
||||
assert.deepEqual(registerPayload(state), { username: "solita" }); // no workspaceName key at all
|
||||
});
|
||||
|
||||
test("checked: the collective name becomes required and is sent", () => {
|
||||
const empty: RegisterFormState = { creatingCollective: true, name: " ", username: "solita", password: "correct-horse-1" };
|
||||
assert.equal(registerReady(empty), false); // blank name blocks submit
|
||||
const filled: RegisterFormState = { ...empty, name: "Acampada 26S" };
|
||||
assert.equal(registerReady(filled), true);
|
||||
assert.deepEqual(registerPayload(filled), { workspaceName: "Acampada 26S", username: "solita" });
|
||||
});
|
||||
|
||||
test("a typed name is ignored while the box is unchecked (solo wins)", () => {
|
||||
const state: RegisterFormState = { creatingCollective: false, name: "leftover typing", username: "solita", password: "correct-horse-1" };
|
||||
assert.equal(registerReady(state), true);
|
||||
assert.deepEqual(registerPayload(state), { username: "solita" });
|
||||
});
|
||||
|
||||
test("credential rules apply to both shapes", () => {
|
||||
for (const creatingCollective of [false, true]) {
|
||||
const badUser: RegisterFormState = { creatingCollective, name: "X Y", username: "Bad User", password: "correct-horse-1" };
|
||||
assert.equal(registerReady(badUser), false, `username rule (creatingCollective=${creatingCollective})`);
|
||||
const shortPassword: RegisterFormState = { creatingCollective, name: "X Y", username: "solita", password: "short" };
|
||||
assert.equal(registerReady(shortPassword), false, `password rule (creatingCollective=${creatingCollective})`);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue