Files
core/src/features/auth/README-custom-login.md
T
hykocx 81172bda94 chore: rename package from @hykocx/zen to @zen/core
Update all references across source files, documentation, and
configuration to reflect the new package scope and name. This includes
updating `.npmrc` registry config, install instructions, module
examples, and all import path comments throughout the codebase.
2026-04-12 15:09:26 -04:00

9.4 KiB
Raw Blame History

Custom auth pages

This guide explains how to build your own auth pages (login, register, forgot password, reset password, confirm email, logout) so they match your sites layout and style. For a basic site you can keep using the default auth page.

Overview

You can use a custom page for every auth flow:

Page Component Server action(s)
Login LoginPage loginAction, setSessionCookie
Register RegisterPage registerAction
Forgot password ForgotPasswordPage forgotPasswordAction
Reset password ResetPasswordPage resetPasswordAction
Confirm email ConfirmEmailPage verifyEmailAction
Logout LogoutPage logoutAction, setSessionCookie
  • Components: from @zen/core/auth/components
  • Actions: from @zen/core/auth/actions

Create your own routes (e.g. /login, /register, /auth/forgot) and wrap Zens components in your layout. Each page follows the same pattern: a server component that loads data and passes actions, and a client wrapper that handles navigation and renders the Zen component.


Route structure

Choose a URL scheme and use it consistently. Two common options:

Option A All under /auth/* (like the default)
/auth/login, /auth/register, /auth/forgot, /auth/reset, /auth/confirm, /auth/logout

Option B Top-level routes
/login, /register, /forgot, /reset, /confirm, /logout

The onNavigate callback receives one of: 'login' | 'register' | 'forgot' | 'reset'. Map each to your chosen path, e.g. router.push(\/auth/${page}`)orrouter.push(`/${page}`)`.

Reset and confirm pages need email and token from the URL (e.g. /auth/reset?email=...&token=...). Your server page can read searchParams and pass them to the component.


Component reference (props)

Use this when wiring each custom page.

Component Props
LoginPage onSubmit (loginAction), onSetSessionCookie, onNavigate, redirectAfterLogin, currentUser
RegisterPage onSubmit (registerAction), onNavigate, currentUser
ForgotPasswordPage onSubmit (forgotPasswordAction), onNavigate, currentUser
ResetPasswordPage onSubmit (resetPasswordAction), onNavigate, email, token (from URL)
ConfirmEmailPage onSubmit (verifyEmailAction), onNavigate, email, token (from URL)
LogoutPage onLogout (logoutAction), onSetSessionCookie (optional)

1. Login

Server: app/login/page.js (or app/auth/login/page.js)

import { getSession, loginAction, setSessionCookie } from '@zen/core/auth/actions';
import { LoginPageWrapper } from './LoginPageWrapper';

export default async function LoginRoute() {
  const session = await getSession();
  return (
    <YourSiteLayout>
      <LoginPageWrapper
        loginAction={loginAction}
        setSessionCookie={setSessionCookie}
        currentUser={session?.user ?? null}
      />
    </YourSiteLayout>
  );
}

Client: app/login/LoginPageWrapper.js

'use client';

import { useRouter } from 'next/navigation';
import { LoginPage } from '@zen/core/auth/components';

export function LoginPageWrapper({ loginAction, setSessionCookie, currentUser }) {
  const router = useRouter();
  return (
    <LoginPage
      onSubmit={loginAction}
      onSetSessionCookie={setSessionCookie}
      onNavigate={(page) => router.push(`/auth/${page}`)}
      redirectAfterLogin="/"
      currentUser={currentUser}
    />
  );
}

2. Register

Server: app/register/page.js

import { getSession, registerAction } from '@zen/core/auth/actions';
import { RegisterPageWrapper } from './RegisterPageWrapper';

export default async function RegisterRoute() {
  const session = await getSession();
  return (
    <YourSiteLayout>
      <RegisterPageWrapper
        registerAction={registerAction}
        currentUser={session?.user ?? null}
      />
    </YourSiteLayout>
  );
}

Client: app/register/RegisterPageWrapper.js

'use client';

import { useRouter } from 'next/navigation';
import { RegisterPage } from '@zen/core/auth/components';

export function RegisterPageWrapper({ registerAction, currentUser }) {
  const router = useRouter();
  return (
    <RegisterPage
      onSubmit={registerAction}
      onNavigate={(page) => router.push(`/auth/${page}`)}
      currentUser={currentUser}
    />
  );
}

3. Forgot password

Server: app/forgot/page.js

import { getSession, forgotPasswordAction } from '@zen/core/auth/actions';
import { ForgotPasswordPageWrapper } from './ForgotPasswordPageWrapper';

export default async function ForgotRoute() {
  const session = await getSession();
  return (
    <YourSiteLayout>
      <ForgotPasswordPageWrapper
        forgotPasswordAction={forgotPasswordAction}
        currentUser={session?.user ?? null}
      />
    </YourSiteLayout>
  );
}

Client: app/forgot/ForgotPasswordPageWrapper.js

'use client';

import { useRouter } from 'next/navigation';
import { ForgotPasswordPage } from '@zen/core/auth/components';

export function ForgotPasswordPageWrapper({ forgotPasswordAction, currentUser }) {
  const router = useRouter();
  return (
    <ForgotPasswordPage
      onSubmit={forgotPasswordAction}
      onNavigate={(page) => router.push(`/auth/${page}`)}
      currentUser={currentUser}
    />
  );
}

4. Reset password

Requires email and token from the reset link (e.g. /auth/reset?email=...&token=...). Read searchParams in the server component and pass them to the client.

Server: app/auth/reset/page.js (or app/reset/page.js with dynamic segment if needed)

import { resetPasswordAction } from '@zen/core/auth/actions';
import { ResetPasswordPageWrapper } from './ResetPasswordPageWrapper';

export default async function ResetRoute({ searchParams }) {
  const params = typeof searchParams?.then === 'function' ? await searchParams : searchParams ?? {};
  const email = params.email ?? '';
  const token = params.token ?? '';
  return (
    <YourSiteLayout>
      <ResetPasswordPageWrapper
        resetPasswordAction={resetPasswordAction}
        email={email}
        token={token}
      />
    </YourSiteLayout>
  );
}

Client: app/auth/reset/ResetPasswordPageWrapper.js

'use client';

import { useRouter } from 'next/navigation';
import { ResetPasswordPage } from '@zen/core/auth/components';

export function ResetPasswordPageWrapper({ resetPasswordAction, email, token }) {
  const router = useRouter();
  return (
    <ResetPasswordPage
      onSubmit={resetPasswordAction}
      onNavigate={(page) => router.push(`/auth/${page}`)}
      email={email}
      token={token}
    />
  );
}

5. Confirm email

Requires email and token from the verification link (e.g. /auth/confirm?email=...&token=...).

Server: app/auth/confirm/page.js

import { verifyEmailAction } from '@zen/core/auth/actions';
import { ConfirmEmailPageWrapper } from './ConfirmEmailPageWrapper';

export default async function ConfirmRoute({ searchParams }) {
  const params = typeof searchParams?.then === 'function' ? await searchParams : searchParams ?? {};
  const email = params.email ?? '';
  const token = params.token ?? '';
  return (
    <YourSiteLayout>
      <ConfirmEmailPageWrapper
        verifyEmailAction={verifyEmailAction}
        email={email}
        token={token}
      />
    </YourSiteLayout>
  );
}

Client: app/auth/confirm/ConfirmEmailPageWrapper.js

'use client';

import { useRouter } from 'next/navigation';
import { ConfirmEmailPage } from '@zen/core/auth/components';

export function ConfirmEmailPageWrapper({ verifyEmailAction, email, token }) {
  const router = useRouter();
  return (
    <ConfirmEmailPage
      onSubmit={verifyEmailAction}
      onNavigate={(page) => router.push(`/auth/${page}`)}
      email={email}
      token={token}
    />
  );
}

6. Logout

Server: app/auth/logout/page.js

import { logoutAction, setSessionCookie } from '@zen/core/auth/actions';
import { LogoutPageWrapper } from './LogoutPageWrapper';

export default function LogoutRoute() {
  return (
    <YourSiteLayout>
      <LogoutPageWrapper
        logoutAction={logoutAction}
        setSessionCookie={setSessionCookie}
      />
    </YourSiteLayout>
  );
}

Client: app/auth/logout/LogoutPageWrapper.js

'use client';

import { LogoutPage } from '@zen/core/auth/components';

export function LogoutPageWrapper({ logoutAction, setSessionCookie }) {
  return (
    <LogoutPage
      onLogout={logoutAction}
      onSetSessionCookie={setSessionCookie}
    />
  );
}

Protecting routes

Use protect() from @zen/core/auth and set redirectTo to your custom login path:

import { protect } from '@zen/core/auth';

export const middleware = protect({ redirectTo: '/login' });

So unauthenticated users are sent to your custom login page.


Default auth page

If you dont need a custom layout, keep using the built-in auth UI. In app/auth/[...auth]/page.js:

export { default } from '@zen/core/auth/page';

This serves login, register, forgot, reset, confirm, and logout under /auth/* with the default styling.