@dloizides/auth-web

中文文档
1.31.0发布于 2026/11/10MITTypeScriptESM + CJS零依赖近 30 天下载 2914

Themeable, branded auth UI for the dloizides.com portfolio. Native LoginForm / ForgotPasswordForm / ResetPasswordForm components, headless hooks, the same-origin BffAuthClient, and a role-based post-login router. Built on @dloizides/auth-client; talks onl

原始使用文档
文档目录15 节
  1. Install
  2. Two ways to consume it
  3. 1. Ready-made themeable components
  4. Email-OTP login — <OtpForm>
  5. Event-PIN login — <PinForm>
  6. 2. Headless hooks (custom layout)
  7. Theming
  8. Public demo credentials — <DemoCredentialsHint>
  9. Async credentials: prefillOnMount honours a late arrival
  10. It fails closed
  11. The prefill is a seed, not a leash
  12. Internationalisation
  13. Role-based post-login routing
  14. API surface
  15. License

@dloizides/auth-web

Themeable, branded auth UI for the dloizides.com portfolio — the frontend half of the unified-auth plan.

Every app gets a native, branded login experience: the login / forgot / reset forms live inside the app's own frontend; the user is never redirected to Keycloak's hosted login UI. All credential exchange happens server-side in a per-app BFF (bff-katalogos, bff-erevna, ...). This package talks only to a same-origin /bff/* — no secrets, no token handling, no Keycloak calls in the browser.

Built on @dloizides/auth-client, which provides the lower-level BffAuthClient.

Install

npm install @dloizides/auth-web

Peer dependencies: react, react-native (optional), @tanstack/react-query, @dloizides/auth-client (>=3.0.0).

Two ways to consume it

1. Ready-made themeable components

import {
  AuthThemeProvider,
  LoginForm,
  createBffAuthClient,
  resolvePostLoginRoute,
} from '@dloizides/auth-web';

import { katalogosAuthTheme } from './theme';      // your AuthTheme token bag
import { roleRoutes } from './roleRoutes';          // your RoleRouteTable
import { authLabels } from './authLabels';          // your localised labels

const client = createBffAuthClient();               // same-origin /bff/*

function LoginScreen() {
  const router = useRouter();
  return (
    <AuthThemeProvider theme={katalogosAuthTheme}>
      <LoginForm
        client={client}
        labels={authLabels.login}
        onForgotPassword={() => router.push('/forgot-password')}
        onSuccess={(user) => {
          const route = resolvePostLoginRoute(user, roleRoutes);
          router.replace(route ?? '/no-access');
        }}
      />
    </AuthThemeProvider>
  );
}

<ForgotPasswordForm> and <ResetPasswordForm> follow the same shape.

Email-OTP login — <OtpForm>

A native, branded "sign in with a code" surface — the user is never bounced to Keycloak's hosted UI. It is a two-step form: step 1 collects the email and asks the BFF to email a one-time code; step 2 collects the code, verifies it, and offers "resend code" / "use a different email".

import { OtpForm, createBffAuthClient, resolvePostLoginRoute } from '@dloizides/auth-web';

const client = createBffAuthClient();

function OtpLoginScreen() {
  const router = useRouter();
  return (
    <OtpForm
      client={client}
      labels={authLabels.otp}
      onSuccess={(user) => {
        const route = resolvePostLoginRoute(user, roleRoutes);
        router.replace(route ?? '/no-access');
      }}
    />
  );
}

<OtpForm> POSTs to the same-origin /bff/otp/request and /bff/otp/verify endpoints (added in Bff.AspNetCore). The BFF runs the OTP direct-grant against Keycloak server-side; the browser receives only the httpOnly session cookie.

Event-PIN login — <PinForm>

A native, branded "sign in with your event PIN" surface for operational staff (door / DJ / media on Kefi). It is a single-step form: a PIN field + a "sign in" button. The eventExternalId is a prop — the event context comes from the route/page, never typed by the user. The (event, pin) pair alone identifies the staff member; no username/password ever leaves the browser.

import { PinForm, createBffAuthClient, resolvePostLoginRoute } from '@dloizides/auth-web';

const client = createBffAuthClient();

function PinLoginScreen({ eventExternalId }: { eventExternalId: string }) {
  const router = useRouter();
  return (
    <PinForm
      client={client}
      eventExternalId={eventExternalId}
      labels={authLabels.pin}
      onSuccess={(user) => {
        const route = resolvePostLoginRoute(user, roleRoutes);
        router.replace(route ?? '/no-access');
      }}
    />
  );
}

<PinForm> POSTs to the same-origin /bff/pin/login endpoint (added in Bff.AspNetCore). The BFF runs the event-scoped PIN direct-grant against Keycloak server-side; the browser receives only the httpOnly session cookie.

2. Headless hooks (custom layout)

import { useBffAuth, createBffAuthClient } from '@dloizides/auth-web';

const client = createBffAuthClient();

function CustomLogin() {
  const { login, isSubmitting, error } = useBffAuth({ client, probeOnMount: false });
  // ...render your own form, call login({ username, password })
}

For a custom OTP layout, useOtpLogin exposes the two-step machine:

import { useOtpLogin, OtpLoginStep, createBffAuthClient } from '@dloizides/auth-web';

const client = createBffAuthClient();

function CustomOtpLogin() {
  const otp = useOtpLogin({ client });
  // step 1: otp.requestCode(email)  → otp.step becomes OtpLoginStep.EnterCode
  // step 2: otp.verifyCode(code)    → resolves to the signed-in BffUser
  //         otp.resend() / otp.reset() for the step-2 affordances
}

For a custom PIN layout, usePinLogin exposes the single-step flow:

import { usePinLogin, createBffAuthClient } from '@dloizides/auth-web';

const client = createBffAuthClient();

function CustomPinLogin({ eventExternalId }: { eventExternalId: string }) {
  const pin = usePinLogin({ client, eventExternalId });
  // pin.submit(pinValue) → resolves to the signed-in BffUser
  // pin.reset()          → clears the error
}

Theming

The package owns no brand. Each app maps its own theme system onto the flat AuthTheme token bag (colors, radii, spacing, typography) and supplies it via <AuthThemeProvider> or a theme prop on an individual component. Precedence: prop → context → defaultAuthTheme.

import { defaultAuthTheme, type AuthTheme } from '@dloizides/auth-web';

export const katalogosAuthTheme: AuthTheme = {
  ...defaultAuthTheme,
  colors: { ...defaultAuthTheme.colors, primary: '#c2410c' },
};

Because all three forms share one useAuthStyles token-to-style mapping, re-theming <LoginForm> automatically re-themes the others.

Public demo credentials — <DemoCredentialsHint>

For a product with an OPEN demo account whose credentials are already published (typically on the marketing site). It shows the username + password on the login card and offers a one-tap fill, so a non-technical evaluator never copy-pastes a password between two tabs.

It fills fields, nothing more. The login then travels the ordinary path — same BFF, same Keycloak, same session cookie, same roles. There is no auth bypass and no credential inside this package: the host supplies the pair, and the demo account's blast radius is whatever the IdP grants it.

The demo pair comes from the BFF (GET /bff/config → config.demo), not a client const — the BFF is the only thing that knows whether a deployment has a demo configured, and a bundled const would ship credentials into every customer's self-hosted build. useBffLoginConfig already fetches that config, so it also carries demo. useDemoCredentials owns the hint↔form wiring:

import {
  AuthScreen, DemoCredentialsHint, LoginForm,
  useBffLoginConfig, useDemoCredentials,
} from '@dloizides/auth-web';

function LoginScreen() {
  const { config } = useBffLoginConfig(bffAuthClient);
  // Credentials arrive ASYNC — prefillOnMount honours a late arrival (see below).
  const demo = useDemoCredentials({ credentials: config.demo ?? undefined, prefillOnMount: true });

  return (
    <AuthScreen theme={authTheme}>
      <DemoCredentialsHint
        {...demo.hintProps}
        labels={authLabels.demo}
        testIdPrefix="zygos"
        theme={authTheme}
      />
      <LoginForm
        {...demo.loginFormProps}
        chromeless
        client={bffAuthClient}
        labels={authLabels.login}
        theme={authTheme}
        onSuccess={handleSignedIn}
      />
    </AuthScreen>
  );
}

hintProps carries { credentials, onUse }; loginFormProps carries { initialUsername, initialPassword, prefillNonce, prefillIntent }. A module-const demo works too — just pass it as credentials instead of config.demo.

Requires @dloizides/auth-client >= 4.2.0, which surfaces demo on BffLoginConfig (mapped from the wire's publishedUsername / publishedPassword). config.demo is DemoCredentials | null.

Async credentials: prefillOnMount honours a late arrival

Because the pair arrives a render or two after mount, prefillOnMount cannot lean on useState's initial value (React reads it on the first render only). The hook seeds the form when config.demo resolves to a usable pair — but as an opportunistic seed: <LoginForm> applies it only while both fields are still empty, so a late /bff/config response never overwrites a username a real account holder was typing meanwhile. Tapping Use these credentials is an explicit seed and always applies. Seeding is gated by value, so a react-query refetch returning an equal-valued object does not re-seed. useDemoCredentials chooses the intent for you via the prefillIntent prop; you only touch PrefillIntent if you drive <LoginForm> without the hook.

prefillOnMount (default false) chooses the trade:

Fields on arrivalTo sign in as the demo userCost
falseemptytap Use these credentials, then Sign in—
truefilledtap Sign ina real account holder must clear two fields first

Use true only on a genuinely public demo surface. The panel renders either way, so a visitor can always see why the fields are populated.

It fails closed

No credentials — or a half-configured pair (blank username, blank password, or a field that is undefined at runtime because an env var was never set) — renders nothing at all: zero DOM. A misconfigured demo can never publish an empty credentials panel. resolveDemoCredentials is exported if you need the same decision elsewhere.

The prefill is a seed, not a leash

initialUsername / initialPassword choose the fields' starting value and then get out of the way: every field stays editable and clearable, so a real account holder can always wipe the demo values and sign in with their own. This is why prefillNonce exists — React ignores a useState initial argument on re-render, so changing initial* alone does nothing once mounted, and a "fill the form" button wired without the nonce would leave the form stubbornly empty. useDemoCredentials manages it for you; drive it yourself only if you are not using the hook.

Internationalisation

@dloizides/auth-web ships no i18n framework. Every user-facing string is supplied through a typed labels prop. Apps pass strings already localised with their own FM() / t(). Each label bag is partial — unspecified keys fall back to the English DEFAULT_* constants.

Role-based post-login routing

import { resolvePostLoginRoute, type RoleRouteTable } from '@dloizides/auth-web';

const roleRoutes: RoleRouteTable = {
  routes: [
    { role: 'superUser', route: '/admin/super' },
    { role: 'admin',     route: '/admin' },
    { role: 'user',      route: '/dashboard' },
  ],
  fallback: '/no-access',
};

// The first table entry whose role the user holds wins — list most privileged first.
const route = resolvePostLoginRoute(user, roleRoutes);

API surface

ExportKind
LoginForm, ForgotPasswordForm, ResetPasswordForm, OtpForm, PinFormComponents
AuthScreen, DemoCredentialsHint, PreferredMethodHintComposition + login-surface panels
useDemoCredentials, resolveDemoCredentials, DemoCredentials, PrefillIntentPublic demo credentials
AuthThemeProvider, useAuthTheme, defaultAuthTheme, AuthThemeTheming
DEFAULT_LOGIN_LABELS, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_RESET_PASSWORD_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PIN_LABELSLabel bags
useBffAuth, useBffForgotPassword, useBffResetPassword, useResetPasswordForm, useOtpLogin, usePinLoginHeadless hooks
OtpLoginStepOTP step enum
createBffAuthClient, BffAuthClient (re-export)Client
resolvePostLoginRoute, collectUserRoles, RoleRouteTableRouter
validatePasswordPolicy, isPasswordValid, PasswordPolicyErrorPassword policy
AuthTestIds, withTestIdPrefixTest IDs

License

MIT

版本与发布记录

@dloizides/auth-web 共发布 41 个版本,首次发布于 2026/05/23;过去 12 个月发布了 41 个版本。

版本发布日期距今
1.31.0latest2026/11/102 天前
1.30.02026/11/102 天前
1.29.02026/11/093 天前
1.28.02026/11/093 天前
1.27.02026/11/093 天前
1.26.02026/11/083 天前

关于 @dloizides/auth-web 的常见问题

@dloizides/auth-web 是什么?

Themeable, branded auth UI for the dloizides.com portfolio. Native LoginForm / ForgotPasswordForm / ResetPasswordForm components, headless hooks, the same-origin BffAuthClient, and a role-based post-login router. Built on @dloizides/auth-client; talks onl

如何安装 @dloizides/auth-web?

在项目目录运行 npm install @dloizides/auth-web,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=17.0.0)、@dloizides/ui-buttons(>=1.9.0)、@tanstack/react-query(^5.0.0)、@dloizides/auth-client(>=4.2.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@dloizides/auth-web 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dloizides/auth-web 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@dloizides/auth-web 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@dloizides/auth-web 可以商用吗?

@dloizides/auth-web 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@dloizides/auth-web 的下载量有多少?

根据 npm 官方下载统计,@dloizides/auth-web 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,914 次,日均约 97 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dloizides/auth-web 还在维护吗?

最新版本 1.31.0 发布于 2026/11/10(2 天前);至今共发布 41 个版本,其中过去 12 个月发布了 41 个;首次发布于 2026/05/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。