fix(auth): show only known sign-in error codes

Sign-in errors arrive in the query string, so unknown codes get a fixed
message instead of being echoed into the banner.
This commit is contained in:
Alexa
2026-10-25 15:54:51 +02:00
parent 891ec89899
commit 5bfa463528
4 changed files with 13 additions and 6 deletions
+3 -2
View File
@@ -54,7 +54,7 @@ export function Login() {
const errorText = (e: unknown, codes: Record<string, React.ReactNode>): React.ReactNode => {
if (e instanceof ApiError && e.body && typeof e.body === 'object' && 'error' in e.body) {
const code = (e.body as { error: string }).error;
return codes[code] ?? e.message;
return Object.hasOwn(codes, code) ? codes[code] : e.message;
}
return e instanceof Error ? e.message : String(e);
};
@@ -75,7 +75,8 @@ export function Login() {
ssoHandled.current = true;
setErr(signinError === 'not_linked'
? <>This sign-in isn't linked to a DorkNet account yet. Link it on the <a className="underline" href={siteUrl('/api/auth/external/login?returnUrl=%2Faccount')}>website</a> first, then sign in here again.</>
: SSO_ERRORS[signinError] ?? `Single sign-on failed (${signinError}).`);
// Only known codes are shown: the query string is attacker-controllable.
: Object.hasOwn(SSO_ERRORS, signinError) ? SSO_ERRORS[signinError] : 'Single sign-on failed. Try again.');
setParams({}, { replace: true });
return;
}
@@ -2,7 +2,7 @@ import { NavLink, Link, useLocation, useNavigate, useSearchParams } from 'react-
import { useEffect, useRef, useState } from 'react';
import { BrandMark } from './BrandMark';
import { getDownloads, SignInRequiredError } from '../lib/downloads';
import { getMe, signInUrl, signOut, SIGNIN_ERRORS, type AccountMe } from '../lib/account';
import { errorCopy, getMe, signInUrl, signOut, SIGNIN_ERRORS, type AccountMe } from '../lib/account';
const NAV = [
{ to: '/', label: 'Home', exact: true },
@@ -154,7 +154,7 @@ export function Header() {
{signinError && (
<div className="border-t border-danger/30 bg-danger/10">
<div className="mx-auto max-w-6xl px-4 sm:px-6 py-2 flex items-center justify-between gap-3 text-sm text-danger">
<span>Sign-in didn't work: {SIGNIN_ERRORS[signinError] ?? signinError}</span>
<span>Sign-in didn't work: {errorCopy(SIGNIN_ERRORS, signinError, 'something went wrong. Try again.')}</span>
<button type="button" className="btn-ghost !px-2 !py-1 text-xs" onClick={() => { params.delete('signin_error'); setParams(params, { replace: true }); }}>Dismiss</button>
</div>
</div>
+6
View File
@@ -46,6 +46,12 @@ export const LINK_ERRORS: Record<string, string> = {
no_pending_identity: 'Your sign-in expired. Start again with "Sign in".',
};
/** Friendly copy for a known error code, else `fallback`. For codes read from
* the query string, pass a fixed fallback: anyone can put text in a link. */
export function errorCopy(map: Record<string, string>, code: string | null | undefined, fallback: string): string {
return code && Object.hasOwn(map, code) ? map[code] : fallback;
}
export const SIGNIN_ERRORS: Record<string, string> = {
account_unavailable: 'The account linked to this sign-in is unavailable (deleted or banned).',
no_identity: "The sign-in provider didn't return an identity. Try again.",
+2 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { Link as RouterLink, useNavigate, useSearchParams } from 'react-router';
import { getMe, link, LINK_ERRORS, localPath, signInUrl, type AccountMe } from '../lib/account';
import { errorCopy, getMe, link, LINK_ERRORS, localPath, signInUrl, type AccountMe } from '../lib/account';
// After a first sign-in with the provider: prove which DorkNet account this
// identity belongs to, once, with that account's password.
@@ -35,7 +35,7 @@ export function Link() {
window.location.assign(returnUrl); // the header re-reads who is signed in
} catch (e) {
const key = (e as Error).message;
setErr(LINK_ERRORS[key] ?? key);
setErr(errorCopy(LINK_ERRORS, key, key));
} finally {
setBusy(false);
}