@alphinex/auth-ui
v0.4.0
Published
Reusable authentication pages (Login, and more to follow) built on @alphinex/auth, @alphinex/api-laravel, @alphinex/forms, and @alphinex/layouts.
Readme
@alphinex/auth-ui
Reusable authentication pages built on @alphinex/auth, @alphinex/api-laravel,
@alphinex/forms, and @alphinex/layouts — so projects on the platform never hand-build
Login/Register/Verify/Forgot/Reset screens from scratch. Compose pre-built pages, override only
what differs for your project, and get the rest for free.
Status
v0.4.0 ships all five self-service auth pages (<LoginPage/>, <RegisterPage/>,
<VerifyEmailPage/>, <ForgotPasswordPage/>, <ResetPasswordPage/>), admin user-management
(<UsersPage/>, UserFormDrawer), self-service <AccountSettingsPage/>, and optional OAuth
(Google/GitHub) buttons on Login/Register.
Three layers of reuse
- Full pages (
<LoginPage/>,<RegisterPage/>,<VerifyEmailPage/>,<ForgotPasswordPage/>,<ResetPasswordPage/>,<UsersPage/>,<AccountSettingsPage/>) — zero-config drop-in for a project that matches the defaults closely. - Composable building blocks (
PasswordField,OnboardingSteps,Recaptcha,OAuthButtons,UserFormDrawer,createUserActionsColumn,createLoginSchema/createRegisterSchema/createForgotPasswordSchema/createResetPasswordSchema/createResendVerificationSchema/createUserFormSchema,getAuthErrorMessage,isSlugError,applyServerFieldErrors,getWebmailInboxUrl) — for a project that wants the same visual language but a different field set or flow. - Headless hooks + adapters (
@alphinex/auth'suseAuth/AuthAdapter,@alphinex/api-laravel'sErrorEnvelopeParser/TokenStore,@alphinex/table'smode: "cursor"DataGrid) — auth-ui builds on these, it doesn't duplicate them.
Every page's actual backend call (login via AuthAdapter, register, verify,
forgotPassword, resetPassword, resendVerification) is a prop you supply — auth-ui has no
opinion on your API paths, only on the page flow around them. Route params for
VerifyEmailPage/ResetPasswordPage (userId/hash/search) are passed in as plain props too,
so this package has no dependency on any particular router.
Usage
import {
AuthUIProvider,
LoginPage,
RegisterPage,
createSwitchableTokenStore,
} from "@alphinex/auth-ui";
import { authAdapter } from "./platform/authentication/auth-adapter";
import { apiClient } from "./platform/api/client";
const tokenStore = createSwitchableTokenStore("myapp.auth.token");
function App() {
return (
<AuthUIProvider
authAdapter={authAdapter}
tokenStore={tokenStore}
recaptcha={{ siteKey: import.meta.env.VITE_RECAPTCHA_SITE_KEY }}
shell={{ logo: <img src="/logo.png" alt="My App" className="mx-auto h-12 w-auto" /> }}
>
<LoginPage forgotPasswordHref="/forgot-password" onSuccess={() => navigate("/")} />
</AuthUIProvider>
);
}
function Register() {
return (
<RegisterPage
register={(values) => apiClient.post("/authentication/register", values)}
resendVerification={(email) =>
apiClient.post("/authentication/resend-verification", { email })
}
tenancy={{ resolveRootDomain: () => window.location.hostname }}
loginHref="/login"
/>
);
}tokenStore must be the same instance passed to your createLaravelApiClient({ bearer: {
tokenStore, ... } }) call — AuthUIProvider only toggles its persistence mode via the
"remember me" checkbox, it doesn't own request auth itself.
OAuth (Google/GitHub) — off by default, opt in per project or per page
Every page ships the capability; nothing renders unless you configure it. AuthUIProvider's
oauth prop sets an app-wide default (omit it — the default — and no page shows OAuth buttons
at all); either <LoginPage/> or <RegisterPage/> can override that with their own oauth prop,
including oauth={[]} to turn OAuth off on just that one page even if the provider has it
configured. auth-ui never talks to Google/GitHub itself — each button is a plain link to a URL
your own backend owns (the redirect-start endpoint), not a client-side OAuth SDK:
<AuthUIProvider
authAdapter={authAdapter}
oauth={[
{ provider: "google", href: "/api/auth/google/redirect" },
{ provider: "github", href: "/api/auth/github/redirect" },
]}
>
<LoginPage /> {/* shows both buttons, "Or continue with email", then the form */}
<RegisterPage oauth={[]} /> {/* email/password only, even though the provider has oauth set */}
</AuthUIProvider>"google"/"github" get a built-in icon and label; any other provider string is a custom
provider — supply your own label/icon for those.
Page-by-page overrides
Every page accepts copy (merged over defaults), onError (return true to suppress the
default error banner), and Shell (defaults to AuthLayout wired to the provider's shell
config). See each page's *Props type for the full list; highlights:
<LoginPage/>—extraFields,schema(prefercreateLoginSchema({ extraFields })),onSuccess,recaptcha,forgotPasswordHref,oauth(overrides the provider default).<RegisterPage/>—register(required),tenancy(required —resolveRootDomain/previewWorkspaceUrl/reservedSlugs, since the tenancy model has no universal default),resendVerification,extraFields,schema,loginHref,showOnboardingSteps,oauth(overrides the provider default).<VerifyEmailPage/>—verify(required),params(required —{ userId, hash, search }, extracted from your router and passed in),forwardQueryString(defaultstrue— correctness requirement for signed routes, not a style choice),resendVerification,loginHref.<ForgotPasswordPage/>—forgotPassword(required),loginHref,resendCooldownSeconds. Always shows the same success state regardless of whether the account exists — see "no user enumeration" below.<ResetPasswordPage/>—resetPassword(required),params(required),forwardQueryString,invalidLinkErrorCode(defaults"AUTH_RESET_LINK_INVALID"),loginHref,forgotPasswordHref.<UsersPage/>—columnsanddata(required — cursor mode, so you own fetching/cursor state, same as@alphinex/table'smode: "cursor"),fields(required — the create/edit drawer's field-set config, since a "user" shape varies too much per project to assume one),onCreate/onUpdate/onDelete(each optional — omitting one hides its corresponding UI: noonCreatemeans no "Add user" button, noonDeletemeans no delete action, etc.),canCreate/canEdit/canDeletefor permission gating (pass your ownusePermission()check),hasNextPage/hasPreviousPage/onLoadMore/onLoadPrevious/onQueryChange(forwarded straight to the underlyingDataGrid).
import { UsersPage, createUserFormSchema } from "@alphinex/auth-ui";
import { usePermission } from "@alphinex/permissions";
function Users() {
const [cursor, setCursor] = useState<CursorState>();
return (
<UsersPage
columns={[
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
{ accessorKey: "role", header: "Role" },
]}
data={cursor?.rows ?? []}
hasNextPage={cursor?.hasNextPage}
hasPreviousPage={cursor?.hasPreviousPage}
onLoadMore={() => fetchUsers(cursor?.nextCursor)}
onLoadPrevious={() => fetchUsers(cursor?.previousCursor)}
fields={[
{ name: "name", label: "Name", required: true },
{ name: "email", label: "Email", type: "email", required: true },
{
name: "role",
label: "Role",
type: "select",
required: true,
options: [
{ label: "Admin", value: "admin" },
{ label: "Member", value: "member" },
],
},
]}
onCreate={(values) => apiClient.post("/authentication/users", values)}
onUpdate={(id, values) => apiClient.patch(`/authentication/users/${id}`, values)}
onDelete={(user) => apiClient.delete(`/authentication/users/${user.id}`)}
canCreate={usePermission("authentication.users.manage")}
/>
);
}<AccountSettingsPage/>— three independent sections (profile, change password, change email), each shown only when itson*callback is provided; there is no default backend call for any of them since what "change my email" means (re-verification? a confirmation email? a current-password check?) is too backend-specific to assume.
import { AccountSettingsPage } from "@alphinex/auth-ui";
function Settings() {
const { user } = useAuth();
return (
<AccountSettingsPage
user={user}
profileFields={[{ name: "name", label: "Name", required: true }]}
onUpdateProfile={(values) => apiClient.patch("/authentication/me", values)}
onChangePassword={(values) => apiClient.post("/authentication/change-password", values)}
onChangeEmail={(values) => apiClient.post("/authentication/change-email", values)}
/>
);
}What's intentionally not overridable
- Error handling always branches on
error.code, never HTTP status or message text —errorMessagesmay only add/replace copy for a code, not change this. <ForgotPasswordPage/>never introduces a way to leak whether an account exists — the success state is identical either way, matching your backend's own (expected) identical response.forwardQueryStringon<VerifyEmailPage/>/<ResetPasswordPage/>defaults totrueand is documented as "only turn off if your backend doesn't sign links this way" — signed-URL query strings must be forwarded verbatim, not re-serialized.
