fix(auth): address R4 S1 code-review findings
- link/upgrade OAuth cancel now redirects to /?link=cancelled (a channel App.tsx surfaces as a toast) instead of the pre-auth /?login=oauth_cancelled banner a signed-in user never sees; a plain sign-in cancel is unchanged - the login banner (suspended / oauth_cancelled) is now state, cleared on the first deliberate action like the verify banner — no more stale lingering - add a "Didn't get the verification email?" resend entry on the sign-in screen so a never-arrived verification mail isn't a dead end (was only reachable from the expired-link banner) - unit-test _oauth_redirect_uri (dev-host derive, unknown-host fallback, prod ignores the Host header — the injection guard) - collapse the 5-deep title/button ternaries into Record<Mode,string> maps
This commit is contained in:
@@ -26,6 +26,23 @@ const PASSWORD_MIN = 10;
|
||||
|
||||
type Mode = "signin" | "register" | "forgot" | "reset" | "resend";
|
||||
|
||||
// Per-mode i18n keys for the card title and the submit button. A flat map (vs nested ternaries)
|
||||
// keeps adding a new mode a one-line change and makes a missing key a compile error.
|
||||
const TITLE_KEY: Record<Mode, string> = {
|
||||
signin: "welcome.auth.signinTitle",
|
||||
register: "welcome.auth.createTitle",
|
||||
forgot: "welcome.auth.forgotTitle",
|
||||
reset: "welcome.auth.resetTitle",
|
||||
resend: "welcome.auth.resendTitle",
|
||||
};
|
||||
const SUBMIT_KEY: Record<Mode, string> = {
|
||||
signin: "welcome.auth.signinButton",
|
||||
register: "welcome.auth.createButton",
|
||||
forgot: "welcome.auth.forgotButton",
|
||||
reset: "welcome.auth.resetButton",
|
||||
resend: "welcome.auth.resendButton",
|
||||
};
|
||||
|
||||
// Public landing + authentication. One scrollable page: a hero with the auth card (email+
|
||||
// password, Google, explicit demo) over a short marketing pitch with feature cards. Shown
|
||||
// whenever the user isn't signed in (see App.tsx). The auth forms talk to /auth/* (see the
|
||||
@@ -267,8 +284,10 @@ function AuthCard() {
|
||||
const resetToken = hashParams.get("reset");
|
||||
const verifyToken = hashParams.get("verify"); // raw token → POST to confirm (new flow)
|
||||
const bounced = params.get("access"); // Google denied → "requested" | "denied"
|
||||
const login = params.get("login"); // Google login blocked → "suspended"
|
||||
const deleted = params.get("deleted"); // self-service account deletion just completed
|
||||
// Google login outcome → "suspended" | "oauth_cancelled". State (not a const) so it clears on the
|
||||
// first deliberate action, like the verify banner — otherwise it lingers stalely on later screens.
|
||||
const [login, setLogin] = useState<string | null>(() => params.get("login"));
|
||||
// Verification outcome: from POSTing the fragment token (new flow), or the legacy
|
||||
// ?verify=ok|invalid redirect (pre-SB3 emails still in flight).
|
||||
const [verify, setVerify] = useState<string | null>(() => params.get("verify"));
|
||||
@@ -321,9 +340,10 @@ function AuthCard() {
|
||||
const reset = () => {
|
||||
setError("");
|
||||
setDone("");
|
||||
// The verify banners are a one-time entry notice — dismiss once the user takes any deliberate
|
||||
// action (mode switch, CTA, submit) so it doesn't linger stalely on later screens.
|
||||
// The verify/login banners are one-time entry notices — dismiss once the user takes any
|
||||
// deliberate action (mode switch, CTA, submit) so they don't linger stalely on later screens.
|
||||
setVerify(null);
|
||||
setLogin(null);
|
||||
};
|
||||
|
||||
async function onSubmit(e: React.FormEvent) {
|
||||
@@ -392,16 +412,7 @@ function AuthCard() {
|
||||
|
||||
const inputCls =
|
||||
"w-full bg-card border border-border rounded-xl px-3 py-2.5 text-sm outline-none focus:border-accent";
|
||||
const titleKey =
|
||||
mode === "register"
|
||||
? "welcome.auth.createTitle"
|
||||
: mode === "forgot"
|
||||
? "welcome.auth.forgotTitle"
|
||||
: mode === "reset"
|
||||
? "welcome.auth.resetTitle"
|
||||
: mode === "resend"
|
||||
? "welcome.auth.resendTitle"
|
||||
: "welcome.auth.signinTitle";
|
||||
const titleKey = TITLE_KEY[mode];
|
||||
|
||||
return (
|
||||
<div className="glass rounded-2xl p-6 sm:p-7 w-full max-w-md lg:justify-self-end">
|
||||
@@ -479,19 +490,7 @@ function AuthCard() {
|
||||
disabled={busy}
|
||||
className="px-4 py-2.5 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 disabled:opacity-50 transition"
|
||||
>
|
||||
{busy
|
||||
? t("welcome.auth.working")
|
||||
: t(
|
||||
mode === "register"
|
||||
? "welcome.auth.createButton"
|
||||
: mode === "forgot"
|
||||
? "welcome.auth.forgotButton"
|
||||
: mode === "reset"
|
||||
? "welcome.auth.resetButton"
|
||||
: mode === "resend"
|
||||
? "welcome.auth.resendButton"
|
||||
: "welcome.auth.signinButton",
|
||||
)}
|
||||
{busy ? t("welcome.auth.working") : t(SUBMIT_KEY[mode])}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -531,6 +530,20 @@ function AuthCard() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* A verification email can be lost or never arrive, so offer a resend path from sign-in
|
||||
too — not only from the expired-link banner (which needs a broken link to appear). */}
|
||||
{mode === "signin" && (
|
||||
<button
|
||||
onClick={() => {
|
||||
reset();
|
||||
setMode("resend");
|
||||
}}
|
||||
className="mt-2 block w-full text-center text-xs text-muted hover:text-fg"
|
||||
>
|
||||
{t("welcome.auth.resendLink")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{mode === "signin" && (
|
||||
<>
|
||||
<div className="flex items-center gap-3 my-4 text-[11px] uppercase tracking-wide text-muted">
|
||||
|
||||
Reference in New Issue
Block a user