npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hazo_auth

v5.1.35

Published

Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations

Readme

hazo_auth - Authentication UI Component Package

A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates hazo_config for configuration management and hazo_connect for data access, enabling future components to stay aligned with platform conventions.

What's New in v5.1.28

Schema Validation, Permission Constants & DX Improvements

  • Schema validation - npx hazo_auth validate now checks SQLite schema: required tables, TEXT ID types, hazo_user_scopes columns, admin permissions, and warns about v4 remnant tables
  • Permission constants - New HAZO_AUTH_PERMISSIONS and ALL_ADMIN_PERMISSIONS exports from both hazo_auth and hazo_auth/client for programmatic permission checks
  • CLI fix - CLI wrapper now sets --conditions react-server in NODE_OPTIONS, fixing "server-only" import errors when running npx hazo_auth validate, init-permissions, etc.
  • Silent permission fix - hazo_get_auth now applies String() normalization to role_id/permission_id comparisons, fixing empty permissions when SQLite returns INTEGER IDs
  • DB-generated IDs - init-permissions no longer generates UUIDs client-side; lets the database generate IDs (supports both TEXT UUID and INTEGER PK schemas)
  • Dev debug info - withAuth 403 responses and UserManagementLayout "Access Denied" view now include permission debug details in development mode
  • Import cleanup - Removed .js extensions from internal imports for better TypeScript/bundler compatibility

What's New in v5.1.27

Mandatory Cookie Prefix - cookie_prefix is now required for all consuming apps.

  • Breaking: get_cookies_config() throws if cookie_prefix is not set in [hazo_auth__cookies] config section
  • Breaking: get_cookie_prefix_edge() throws if HAZO_AUTH_COOKIE_PREFIX env var is not set
  • Validation: npx hazo_auth validate now checks for cookie_prefix configuration
  • Init: .env.local.example template includes HAZO_AUTH_COOKIE_PREFIX as required
  • Docs: shadcn/ui components are bundled — consumers do NOT need to install them separately

What's New in v5.1.26

Consumer Setup Improvements - Five fixes that improve the out-of-box experience for new consumers:

  • Multi-tenancy bypass - enable_multi_tenancy = false (the default) now correctly skips scope/invitation checks in OAuth and post-verification flows. No more redirect loops to /hazo_auth/create_firm for simple apps.
  • Clear SQLite errors - Missing sqlite_path in config now throws a clear error instead of silently falling back to a test fixture database. Unrecognized config keys produce warnings with typo suggestions.
  • Auto-schema creation - npx hazo_auth init now creates the SQLite database with all required tables automatically. Also available standalone via npx hazo_auth init-db. Use npx hazo_auth schema to print the canonical SQL.
  • Auth page images - npx hazo_auth init now copies default login/register/forgot-password images to public/hazo_auth/images/.
  • Graceful image fallback - Visual panels on auth pages fall back to a colored background instead of crashing when images are missing.

What's New in v5.2.0 ⚠️ BREAKING CHANGE

Server/Client Module Separation - Complete fix for "Module not found: Can't resolve 'fs'" errors.

Breaking Change - New Import Path:

// BEFORE (v5.1.x) - Server imports from main entry
import { hazo_get_auth, get_login_config } from "hazo_auth";

// AFTER (v5.2.0) - Server imports from dedicated entry point
import { hazo_get_auth, get_login_config } from "hazo_auth/server-lib";

// Client imports unchanged
import { ProfilePicMenu, use_auth_status } from "hazo_auth/client";
import { cn } from "hazo_auth"; // Still works

Key Changes:

  • New hazo_auth/server-lib entry point - All server-only exports (auth functions, services, config loaders) now here
  • Clean main entry - hazo_auth is now client-safe (components, types, utilities only)
  • Peer dependencies - hazo_config and hazo_connect are now peer dependencies (install in your app)
  • Fixed import path - Uses hazo_config/server (not deprecated hazo_config/dist/lib)

Required Migration:

# 1. Install peer dependencies
npm install hazo_config hazo_connect hazo_logs

# 2. Update imports in your server-side code
# Change: import { hazo_get_auth } from "hazo_auth"
# To:     import { hazo_get_auth } from "hazo_auth/server-lib"

What's New in v5.1.23 🔧

FIX: Server/Client Bundling Issue - Added import "server-only" guards to prevent accidental client bundling.

Key Changes:

  • Server-Only Guards - Added import "server-only" to all server files preventing accidental client bundling
  • hazo_logs v1.0.10 - Upgraded with conditional exports for browser/node environments
  • Client Logging Support - Logs API route now exports POST for client-side log ingestion

What's New in v5.1.5 🐛

CRITICAL BUGFIX: Fixed incomplete migration from v4.x to v5.x - several files were still referencing the deprecated hazo_user_roles table instead of hazo_user_scopes. This release completes the scope-based role assignment architecture introduced in v5.0.

Key Fixes:

  • hazo_get_auth - Now correctly fetches roles from hazo_user_scopes
  • Role IDs - Changed from number[] to string[] (UUIDs) throughout codebase
  • User Management - Updated for scope-based role assignments
  • Cache System - Fixed type inconsistencies with UUID role IDs

If you're on v5.x and experiencing permission/role issues, upgrade to v5.1.5 immediately.

What's New in v5.0 🚀

BREAKING CHANGE: Scope-Based Multi-Tenancy - Complete architectural redesign for simpler, more flexible multi-tenancy!

  • Unified Scope System - Single hazo_scopes table replaces 8 separate tables (1 org + 7 scope levels)
  • Membership-Based - Users assigned to scopes via hazo_user_scopes (not org_id on user record)
  • Invitation System - Built-in invitation flow for onboarding new users to existing scopes
  • Create Firm Flow - New users create their own firm (scope) after email verification
  • Post-Verification Routing - Smart routing after email verification: invitations → create firm → default redirect
  • Unlimited Hierarchy - Flexible parent-child relationships, no fixed depth limit
  • Simpler Architecture - Fewer tables, fewer joins, easier to understand
  • New CLI Command - npx hazo_auth init-permissions for flexible permission setup

Migrating from v4.x? This is a breaking change. Run the migration:

# 1. Backup your database first!
# 2. Run the scope consolidation migration
npm run migrate migrations/009_scope_consolidation.sql

# 3. Update configuration (remove org settings, add invitation/create firm settings)
# 4. Update code (remove org-related API calls and components)
# 5. Test thoroughly

See CHANGE_LOG.md for detailed migration guide, rationale, and breaking changes.

What's New in v2.0

Zero-Config Server Components - Authentication pages now work out-of-the-box with ZERO configuration required!

  • True "Drop In and Use" - Pages initialize everything server-side, no loading state
  • Better Performance - Smaller JS bundles, faster page loads, immediate rendering
  • Flexible API Paths - Customize endpoints globally via HazoAuthProvider context
  • Embeddable Components - MySettings and UserManagement adapt to any layout
  • Sensible Defaults - INI files are now optional, defaults built-in

Also Includes (v1.6.6+)

  • JWT Session Tokens for Edge-Compatible Authentication: Secure Edge Runtime authentication in Next.js proxy/middleware files. See Proxy/Middleware Authentication for details.

Table of Contents


Installation

# Install hazo_auth and required peer dependencies
npm install hazo_auth hazo_config hazo_connect hazo_logs next react react-dom next-auth

# UI peer dependencies (required if using hazo_auth UI components)
npm install hazo_ui lucide-react sonner next-themes @radix-ui/react-accordion @radix-ui/react-alert-dialog @radix-ui/react-avatar @radix-ui/react-checkbox @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-hover-card @radix-ui/react-label @radix-ui/react-select @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs @radix-ui/react-tooltip

Note: next, react, react-dom, next-auth, and UI packages are peer dependencies. Your consuming project controls the versions, preventing duplication and type conflicts.

Next.js config: If using password features, add argon2 to serverExternalPackages in your next.config.mjs:

const nextConfig = {
  serverExternalPackages: ['argon2'],
};

Quick Start

The fastest way to get started is using the CLI commands:

# 1. Install the package and peer dependencies
npm install hazo_auth hazo_config hazo_connect hazo_logs next react react-dom next-auth

# 2. Initialize your project (directories, config, database, images)
npx hazo_auth init

# 3. Configure cookie prefix (REQUIRED)
# Edit config/hazo_auth_config.ini and set a unique prefix:
#   [hazo_auth__cookies]
#   cookie_prefix = myapp_

# 4. Set up environment variables
cp .env.local.example .env.local
# Edit .env.local and set:
#   HAZO_AUTH_COOKIE_PREFIX=myapp_   (MUST match cookie_prefix above)
#   JWT_SECRET=<your-secret>
#   ZEPTOMAIL_API_KEY=<your-key>

# 5. Initialize default permissions and roles
npx hazo_auth init-users

# 6. Generate API routes and pages
npx hazo_auth generate-routes --pages

# 7. Start your dev server
npm run dev

That's it! Visit http://localhost:3000/hazo_auth/login to see the login page.

Tailwind v4 Setup (Required for Tailwind v4 Users)

If you're using Tailwind v4, add this to your globals.css AFTER the tailwindcss import:

@import "tailwindcss";

/* Required: Enable Tailwind to scan hazo_auth package classes */
@source "../node_modules/hazo_auth/dist";

Important: Without this directive, Tailwind classes in hazo_auth components (hover states, colors, spacing) will not be compiled, resulting in broken styling.

CLI Commands

npx hazo_auth init                  # Initialize project (creates dirs, copies config)
npx hazo_auth generate-routes       # Generate API routes only
npx hazo_auth generate-routes --pages  # Generate API routes + pages
npx hazo_auth validate              # Check your setup and configuration
npx hazo_auth --help                # Show all commands

Using Zero-Config Page Components (v2.0+)

NEW in v2.0: All pages are now React Server Components that initialize everything on the server. No configuration, no loading state, no hassle!

// app/login/page.tsx - That's literally it!
import { LoginPage } from "hazo_auth/pages/login";

export default function Page() {
  return <LoginPage />;
}

What happens behind the scenes:

  • ✅ Database connection initialized server-side via hazo_connect singleton
  • ✅ Configuration loaded from hazo_auth_config.ini (or uses sensible defaults)
  • ✅ All props automatically configured
  • ✅ Navbar automatically rendered based on config (no manual wrapping needed)
  • ✅ Page renders immediately - NO loading state!

Available zero-config pages:

| Page | Import | Description | |------|--------|-------------| | LoginPage | hazo_auth/pages/login | Login form with forgot password link | | RegisterPage | hazo_auth/pages/register | Registration with password validation and OAuth support | | ForgotPasswordPage | hazo_auth/pages/forgot_password | Request password reset email | | ResetPasswordPage | hazo_auth/pages/reset_password | Set new password with token | | VerifyEmailPage | hazo_auth/pages/verify_email | Email verification handler | | MySettingsPage | hazo_auth/pages/my_settings | User profile and password change |

Example - Complete Auth Flow:

// app/login/page.tsx
import { LoginPage } from "hazo_auth/pages/login";
export default function Page() {
  return <LoginPage />;
}

// app/register/page.tsx
import { RegisterPage } from "hazo_auth/pages/register";
export default function Page() {
  return <RegisterPage />;
}

// app/settings/page.tsx
import { MySettingsPage } from "hazo_auth/pages/my_settings";
export default function Page() {
  return <MySettingsPage />;
}

Customizing Visual Appearance (Optional):

// All pages accept optional visual props
import { LoginPage } from "hazo_auth/pages/login";

export default function Page() {
  return (
    <LoginPage
      image_src="/custom-login-image.jpg"
      image_alt="My company logo"
      image_background_color="#f0f0f0"
    />
  );
}

Embedding MySettings in Your Dashboard:

// MySettings is now container-agnostic!
import { MySettingsPage } from "hazo_auth/pages/my_settings";

export default function DashboardPage() {
  return (
    <DashboardLayout>
      <Sidebar />
      <main className="p-6">
        <MySettingsPage className="max-w-4xl mx-auto" />
      </main>
    </DashboardLayout>
  );
}

Custom API Paths:

If you use custom API endpoints (not /api/hazo_auth/), wrap your app with HazoAuthProvider:

// app/layout.tsx
import { HazoAuthProvider } from "hazo_auth";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <HazoAuthProvider apiBasePath="/api/v1/auth">
          {children}
        </HazoAuthProvider>
      </body>
    </html>
  );
}

Manual Setup (Advanced)

If you prefer manual control, you can use the layout components directly:

// Import layout components
import { LoginLayout } from "hazo_auth/components/layouts/login";
import { RegisterLayout } from "hazo_auth/components/layouts/register";
import { ForgotPasswordLayout } from "hazo_auth/components/layouts/forgot_password";
import { ResetPasswordLayout } from "hazo_auth/components/layouts/reset_password";
import { EmailVerificationLayout } from "hazo_auth/components/layouts/email_verification";
import { MySettingsLayout } from "hazo_auth/components/layouts/my_settings";
import { UserManagementLayout } from "hazo_auth/components/layouts/user_management";

// Import shared components and hooks from barrel export
import {
  ProfilePicMenu,
  ProfilePicMenuWrapper,
  ProfileStamp,
  use_hazo_auth,
  use_auth_status
} from "hazo_auth/components/layouts/shared";

// Import server-side utilities
import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";

Required Dependencies

Peer Dependencies (Required):

npm install hazo_config hazo_connect hazo_logs

UI Components: All shadcn/ui components are bundled with hazo_auth. You do NOT need to install them separately.

Toast Notifications: Add the Toaster component to your app layout:

// app/layout.tsx
import { Toaster } from "sonner";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  );
}

Client vs Server Imports

hazo_auth provides separate entry points for client and server code to avoid bundling Node.js modules in the browser:

Client Components

For client components (browser-safe, no Node.js dependencies):

// Use hazo_auth/client for client components
import {
  ProfilePicMenu,
  ProfileStamp,
  use_auth_status,
  use_hazo_auth,
  cn
} from "hazo_auth/client";

Server Components / API Routes

For server-side code (API routes, Server Components):

// Use hazo_auth/server-lib for server-side code
import { hazo_get_auth, get_config_value } from "hazo_auth/server-lib";
import { hazo_get_user_profiles } from "hazo_auth/server-lib";

Why This Matters

Server-only code (Node.js APIs like fs, path, database access) must be kept separate from client bundles. The hazo_auth/server-lib entry point:

  • Contains all server-only exports (auth functions, services, config loaders)
  • Includes import "server-only" guard that throws build errors if imported in client code
  • Uses peer dependencies (hazo_config, hazo_connect, hazo_logs) from your app

If you accidentally import from hazo_auth/server-lib in a client component, you'll get a helpful build error instead of a cryptic "Can't resolve 'fs'" message.


Dark Mode / Theming

hazo_auth supports dark mode via CSS custom properties. To enable dark mode:

1. Import the theme CSS

Copy the variables file to your project:

cp node_modules/hazo_auth/src/styles/hazo-auth-variables.css ./app/hazo-auth-theme.css

Import in your globals.css:

@import "./hazo-auth-theme.css";

2. CSS Variables Reference

You can customize the theme by overriding these variables:

:root {
  /* Backgrounds */
  --hazo-bg-subtle: #f8fafc;      /* Light background */
  --hazo-bg-muted: #f1f5f9;       /* Slightly darker background */
  
  /* Text */
  --hazo-text-primary: #0f172a;   /* Primary text */
  --hazo-text-secondary: #334155; /* Secondary text */
  --hazo-text-muted: #64748b;     /* Muted/subtle text */
  
  /* Borders */
  --hazo-border: #e2e8f0;         /* Standard border */
}

.dark {
  /* Dark mode overrides */
  --hazo-bg-subtle: #18181b;
  --hazo-bg-muted: #27272a;
  --hazo-text-primary: #fafafa;
  --hazo-text-secondary: #d4d4d8;
  --hazo-text-muted: #a1a1aa;
  --hazo-border: #3f3f46;
}

The dark class is typically added by next-themes or similar theme providers.


Configuration Setup

After installing the package, you need to set up configuration files in your project root:

1. Copy the example config files to your project root:

cp node_modules/hazo_auth/hazo_auth_config.example.ini ./hazo_auth_config.ini
cp node_modules/hazo_auth/hazo_notify_config.example.ini ./hazo_notify_config.ini

2. Customize the configuration files:

  • Edit hazo_auth_config.ini to configure authentication settings, database connection, UI labels, and more
  • Edit hazo_notify_config.ini to configure email service settings (Zeptomail, SMTP, etc.)

3. Set up environment variables (recommended for sensitive data):

  • Create a .env.local file in your project root
  • Add ZEPTOMAIL_API_KEY=your_api_key_here (if using Zeptomail)
  • Add JWT_SECRET=your_secure_random_string_at_least_32_characters (required for JWT session tokens)
  • Add other sensitive configuration values as needed

Note: JWT_SECRET is required for JWT session token functionality (used for Edge-compatible proxy/middleware authentication). Generate a secure random string at least 32 characters long.

For Google OAuth (optional):

# NextAuth.js configuration (required for OAuth)
NEXTAUTH_SECRET=your_secure_random_string_at_least_32_characters
NEXTAUTH_URL=http://localhost:3000  # Change to production URL in production

# Google OAuth credentials (from Google Cloud Console)
HAZO_AUTH_GOOGLE_CLIENT_ID=your_google_client_id
HAZO_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret

See Google OAuth Setup for detailed instructions.

For Cookie Customization (optional):

# Cookie prefix (prevents conflicts when running multiple apps on localhost)
HAZO_AUTH_COOKIE_PREFIX=myapp_

# Cookie domain (optional, for cross-subdomain sharing)
HAZO_AUTH_COOKIE_DOMAIN=.example.com

These environment variables are required for Edge Runtime (middleware) when using cookie customization. Also set in hazo_auth_config.ini:

[hazo_auth__cookies]
cookie_prefix = myapp_
cookie_domain = .example.com

Important: The configuration files must be located in your project root directory (where process.cwd() points to), not inside node_modules. The package reads configuration from process.cwd() at runtime, so storing them elsewhere (including node_modules/hazo_auth) will break runtime access.


Database Setup

Before using hazo_auth, you need to create the required database tables. The package supports both PostgreSQL (for production) and SQLite (for local development/testing).

PostgreSQL Setup

Run the following SQL scripts in your PostgreSQL database:

1. Create the Profile Source Enum Type

-- Enum type for profile picture source
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');

-- Note: hazo_enum_scope_types was removed in v5.0
-- The unified hazo_scopes table uses a TEXT "level" column instead

2. Create the Organization Table (Multi-Tenancy)

-- Organization table for multi-tenancy (create before hazo_users)
CREATE TABLE hazo_org (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
    root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
    user_limit INTEGER NOT NULL DEFAULT 0,
    active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    created_by UUID,  -- Will reference hazo_users after it's created
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_by UUID
);

CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
CREATE INDEX idx_hazo_org_active ON hazo_org(active);

3. Create the Users Table

-- Main users table
CREATE TABLE hazo_users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email_address TEXT NOT NULL UNIQUE,
    password_hash TEXT,                                   -- NULL for OAuth-only users
    name TEXT,
    email_verified BOOLEAN NOT NULL DEFAULT FALSE,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    login_attempts INTEGER NOT NULL DEFAULT 0,
    last_logon TIMESTAMP WITH TIME ZONE,
    profile_picture_url TEXT,
    profile_source hazo_enum_profile_source_enum,
    mfa_secret TEXT,
    url_on_logon TEXT,
    user_type TEXT,                                       -- Optional user categorization
    google_id TEXT UNIQUE,                                -- Google OAuth ID
    auth_providers TEXT DEFAULT 'email',                  -- 'email', 'google', or 'email,google'
    org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
    root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);

-- Add FK constraints to hazo_org after hazo_users exists
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
    FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
    FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;

Note: The url_on_logon field is used to store a custom redirect URL for users after successful login. This allows per-user customization of post-login navigation.

4. Create the Refresh Tokens Table

-- Refresh tokens table (used for password reset, email verification, etc.)
CREATE TABLE hazo_refresh_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
    token_hash TEXT NOT NULL,
    token_type TEXT NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

-- Index for token lookups
CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);

5. Create the Permissions Table

-- Permissions table for RBAC
CREATE TABLE hazo_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    permission_name TEXT NOT NULL UNIQUE,
    description TEXT,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

6. Create the Roles Table

-- Roles table for RBAC
CREATE TABLE hazo_roles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    role_name TEXT NOT NULL UNIQUE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

7. Create the Role-Permissions Junction Table

-- Junction table linking roles to permissions
CREATE TABLE hazo_role_permissions (
    role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
    permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    PRIMARY KEY (role_id, permission_id)
);

-- Indexes for lookups
CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id);
CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);

8. Create the User-Roles Junction Table

-- Junction table linking users to roles
CREATE TABLE hazo_user_roles (
    user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
    role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    PRIMARY KEY (user_id, role_id)
);

-- Indexes for lookups
CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);

Complete PostgreSQL Setup Script

For convenience, here's the complete SQL script to create all tables at once:

-- ============================================
-- hazo_auth Database Setup Script (PostgreSQL)
-- ============================================

-- 1. Create enum types
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
-- Note: hazo_enum_scope_types was removed in v5.0 (uses unified hazo_scopes table)

-- 2. Create organization table (multi-tenancy)
CREATE TABLE hazo_org (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
    root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
    user_limit INTEGER NOT NULL DEFAULT 0,
    active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    created_by UUID,
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_by UUID
);
CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
CREATE INDEX idx_hazo_org_active ON hazo_org(active);

-- 3. Create users table
CREATE TABLE hazo_users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email_address TEXT NOT NULL UNIQUE,
    password_hash TEXT,
    name TEXT,
    email_verified BOOLEAN NOT NULL DEFAULT FALSE,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    login_attempts INTEGER NOT NULL DEFAULT 0,
    last_logon TIMESTAMP WITH TIME ZONE,
    profile_picture_url TEXT,
    profile_source hazo_enum_profile_source_enum,
    mfa_secret TEXT,
    url_on_logon TEXT,
    user_type TEXT,
    google_id TEXT UNIQUE,
    auth_providers TEXT DEFAULT 'email',
    org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
    root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);

-- Add FK constraints to hazo_org after hazo_users exists
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
    FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
    FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;

-- 4. Create refresh tokens table
CREATE TABLE hazo_refresh_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
    token_hash TEXT NOT NULL,
    token_type TEXT NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);

-- 4. Create permissions table
CREATE TABLE hazo_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    permission_name TEXT NOT NULL UNIQUE,
    description TEXT,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

-- 5. Create roles table
CREATE TABLE hazo_roles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    role_name TEXT NOT NULL UNIQUE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

-- 6. Create role-permissions junction table
CREATE TABLE hazo_role_permissions (
    role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
    permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    PRIMARY KEY (role_id, permission_id)
);
CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id);
CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);

-- 7. Create user-roles junction table
CREATE TABLE hazo_user_roles (
    user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
    role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    PRIMARY KEY (user_id, role_id)
);
CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);

SQLite Setup (for local development)

For local development and testing, you can use SQLite. The SQLite schema is slightly different (no UUID type, TEXT used instead):

-- ============================================
-- hazo_auth Database Setup Script (SQLite)
-- ============================================

-- Users table
CREATE TABLE IF NOT EXISTS hazo_users (
    id TEXT PRIMARY KEY,
    email_address TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    name TEXT,
    email_verified INTEGER NOT NULL DEFAULT 0,
    is_active INTEGER NOT NULL DEFAULT 1,
    login_attempts INTEGER NOT NULL DEFAULT 0,
    last_logon TEXT,
    profile_picture_url TEXT,
    profile_source TEXT,
    mfa_secret TEXT,
    url_on_logon TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    changed_at TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Refresh tokens table
CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
    token_hash TEXT NOT NULL,
    token_type TEXT NOT NULL,
    expires_at TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Permissions table
CREATE TABLE IF NOT EXISTS hazo_permissions (
    id TEXT PRIMARY KEY,
    permission_name TEXT NOT NULL UNIQUE,
    description TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    changed_at TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Roles table
CREATE TABLE IF NOT EXISTS hazo_roles (
    id TEXT PRIMARY KEY,
    role_name TEXT NOT NULL UNIQUE,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    changed_at TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Role-permissions junction table
CREATE TABLE IF NOT EXISTS hazo_role_permissions (
    role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
    permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    changed_at TEXT NOT NULL DEFAULT (datetime('now')),
    PRIMARY KEY (role_id, permission_id)
);

-- User-roles junction table
CREATE TABLE IF NOT EXISTS hazo_user_roles (
    user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
    role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    changed_at TEXT NOT NULL DEFAULT (datetime('now')),
    PRIMARY KEY (user_id, role_id)
);

Initialize Default Permissions and Super User

After creating the tables, you can use the init-users script to set up default permissions and a super user:

npm run init-users

This script reads from hazo_auth_config.ini and:

  1. Creates default permissions from application_permission_list_defaults
  2. Creates a default_super_user_role role with all permissions
  3. Assigns the role to the user specified in default_super_user_email

Apply Migrations

To apply database migrations (e.g., adding new fields):

# Apply a specific migration
npx tsx scripts/apply_migration.ts migrations/003_add_url_on_logon_to_hazo_users.sql

# Or apply all pending migrations
npx tsx scripts/apply_migration.ts

Google OAuth Setup

hazo_auth supports Google Sign-In via NextAuth.js v4, allowing users to authenticate with their Google accounts.

Features

  • Dual authentication: Users can have BOTH Google OAuth and email/password login
  • Auto-linking: Automatically links Google login to existing unverified email/password accounts
  • Graceful degradation: Login and register pages adapt based on enabled authentication methods
  • Set password feature: Google-only users can add a password later via My Settings
  • Profile data: Full name and profile picture automatically populated from Google

Step 1: Get Google OAuth Credentials

  1. Go to Google Cloud Console
  2. Create a project or select an existing project
  3. Enable Google+ API (or Google Identity Services)
  4. Navigate to CredentialsCreate CredentialsOAuth 2.0 Client ID
  5. Configure OAuth consent screen if prompted
  6. Set Application type to "Web application"
  7. Add Authorized JavaScript origins:
    • Development: http://localhost:3000
    • Production: https://yourdomain.com
  8. Add Authorized redirect URIs:
    • Development: http://localhost:3000/api/auth/callback/google
    • Production: https://yourdomain.com/api/auth/callback/google
  9. Copy the Client ID and Client Secret

Step 2: Add Environment Variables

Add to your .env.local:

# NextAuth.js configuration (REQUIRED for OAuth)
NEXTAUTH_SECRET=your_secure_random_string_at_least_32_characters
NEXTAUTH_URL=http://localhost:3000  # Change to production URL in production

# Google OAuth credentials (from Google Cloud Console)
HAZO_AUTH_GOOGLE_CLIENT_ID=your_google_client_id_from_step_1
HAZO_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret_from_step_1

Generate NEXTAUTH_SECRET:

openssl rand -base64 32

Step 3: Run Database Migration

Add OAuth fields to the hazo_users table:

npm run migrate migrations/005_add_oauth_fields_to_hazo_users.sql

This migration adds:

  • google_id - Google's unique user ID (TEXT, UNIQUE)
  • auth_providers - Tracks authentication methods: 'email', 'google', or 'email,google'
  • Index on google_id for fast OAuth lookups

Manual migration (if needed):

PostgreSQL:

ALTER TABLE hazo_users
ADD COLUMN google_id TEXT UNIQUE;

ALTER TABLE hazo_users
ADD COLUMN auth_providers TEXT DEFAULT 'email';

CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);

SQLite:

ALTER TABLE hazo_users
ADD COLUMN google_id TEXT;

ALTER TABLE hazo_users
ADD COLUMN auth_providers TEXT DEFAULT 'email';

CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_users_google_id_unique ON hazo_users(google_id);
CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);

Step 4: Configure OAuth in hazo_auth_config.ini

[hazo_auth__oauth]
# Enable Google OAuth login (default: true)
enable_google = true

# Enable traditional email/password login (default: true)
enable_email_password = true

# Auto-link Google login to existing unverified email/password accounts (default: true)
auto_link_unverified_accounts = true

# Customize button text (optional)
google_button_text = Continue with Google
google_button_text_register = Sign up with Google
oauth_divider_text = or

# Post-Login Redirect Configuration (v5.1.16+)
# URL for users who need to create a firm (default: /hazo_auth/create_firm)
# create_firm_url = /hazo_auth/create_firm

# Default redirect after OAuth login for users with scopes (default: /)
# default_redirect = /

# Skip invitation table check (set true if not using invitations)
# skip_invitation_check = false

# Redirect when skip_invitation_check=true and user has no scope (default: /)
# no_scope_redirect = /

Step 5: Create NextAuth API Routes

Create app/api/auth/[...nextauth]/route.ts:

export { GET, POST } from "hazo_auth/server/routes/nextauth";

Create app/api/hazo_auth/oauth/google/callback/route.ts:

export { GET } from "hazo_auth/server/routes/oauth_google_callback";

Create app/api/hazo_auth/set_password/route.ts:

export { POST } from "hazo_auth/server/routes/set_password";

Or use the CLI generator:

npx hazo_auth generate-routes --oauth

Step 6: Test Google OAuth

  1. Start your dev server: npm run dev
  2. Visit http://localhost:3000/hazo_auth/login
  3. You should see the "Sign in with Google" button
  4. Click it and authenticate with your Google account
  5. You'll be redirected back and logged in

User Flows

New User - Google Sign-In:

  • User clicks "Sign in with Google"
  • Authenticates with Google
  • Account created with Google profile data (email, name, profile picture)
  • Email is automatically verified
  • User can log in with Google anytime

Existing Unverified User - Google Sign-In:

  • User has email/password account but hasn't verified email
  • Clicks "Sign in with Google" with same email
  • System auto-links Google account (if auto_link_unverified_accounts = true)
  • Email becomes verified
  • User can now log in with EITHER Google OR email/password

Google-Only User Adds Password:

  • Google-only user visits My Settings
  • "Set Password" section appears
  • User sets a password
  • User can now log in with EITHER method

Google-Only User Tries Forgot Password:

  • User registered with Google tries "Forgot Password"
  • System shows: "You registered with Google. Please sign in with Google instead."

Configuration Options

Disable email/password login (Google-only):

[hazo_auth__oauth]
enable_google = true
enable_email_password = false

Hide "Create account" link (e.g., OAuth-only apps with no email registration):

[hazo_auth__login_layout]
show_create_account_link = false

Hide links by setting path/label to empty (v5.1.30+):

[hazo_auth__login_layout]
; Hide "Forgot password?" link
forgot_password_path =
forgot_password_label =

; Hide "Create account" link (alternative to show_create_account_link = false)
create_account_path =
create_account_label =

Disable Google OAuth (email/password only):

[hazo_auth__oauth]
enable_google = false
enable_email_password = true

API Response Changes

The /api/hazo_auth/me endpoint now includes OAuth status:

{
  authenticated: true,
  // ... existing fields
  auth_providers: "email,google",  // NEW: Tracks authentication methods
  has_password: true,              // NEW: Whether user has password set
  google_connected: true,          // NEW: Whether Google account is linked
}

Dependencies

Google OAuth adds one new dependency:

  • next-auth@^4.24.11 - NextAuth.js for OAuth handling (automatically installed with hazo_auth)

Troubleshooting

"Sign in with Google" button not showing:

  • Verify enable_google = true in [hazo_auth__oauth] section
  • Check HAZO_AUTH_GOOGLE_CLIENT_ID and HAZO_AUTH_GOOGLE_CLIENT_SECRET are set
  • Check NEXTAUTH_URL matches your current URL

OAuth callback error:

  • OAuth errors (e.g., AccessDenied, OAuthSignin) are automatically displayed as a banner on the login page via ?error= query param
  • Verify redirect URI in Google Cloud Console matches exactly: http://localhost:3000/api/auth/callback/google
  • Check NEXTAUTH_SECRET is set and at least 32 characters
  • Verify API routes are created: /api/auth/[...nextauth]/route.ts and /api/hazo_auth/oauth/google/callback/route.ts

User created but not logged in:

  • Check browser console for errors
  • Verify /api/hazo_auth/oauth/google/callback route exists
  • Check server logs for errors during session creation

OAuth redirect goes to localhost behind a reverse proxy (Cloudflare Tunnel, nginx):

  • Set NEXTAUTH_URL to your public domain (e.g., https://gotimer.org)
  • hazo_auth automatically rewrites the request URL so NextAuth constructs the correct redirect_uri
  • Verify AUTH_TRUST_HOST=true is set in your environment
  • No next.config changes needed — the fix is built into hazo_auth/server/routes

404 after Google OAuth login (v5.1.16+ fix):

  • If users get 404 after Google OAuth, the hazo_invitations table may be missing
  • Option 1: Run migration 009_scope_consolidation.sql to create the table
  • Option 2: Set skip_invitation_check = true in [hazo_auth__oauth] if not using invitations
  • Check logs for invitation_table_missing warnings
  • If using custom paths, set create_firm_url to your app's create firm page URL

Using Components

Package Exports

The package exports components through these paths:

// Main entry point - exports all public APIs
import { ... } from "hazo_auth";

// Zero-config page components (recommended for quick setup)
import { LoginPage } from "hazo_auth/pages/login";
import { RegisterPage } from "hazo_auth/pages/register";
import { ForgotPasswordPage } from "hazo_auth/pages/forgot_password";
import { ResetPasswordPage } from "hazo_auth/pages/reset_password";
import { VerifyEmailPage } from "hazo_auth/pages/verify_email";
import { MySettingsPage } from "hazo_auth/pages/my_settings";

// Or import all pages at once
import { 
  LoginPage, 
  RegisterPage, 
  ForgotPasswordPage,
  ResetPasswordPage,
  VerifyEmailPage,
  MySettingsPage 
} from "hazo_auth/pages";

// Layout components - for custom implementations
import { LoginLayout } from "hazo_auth/components/layouts/login";
import { RegisterLayout } from "hazo_auth/components/layouts/register";
import { ForgotPasswordLayout } from "hazo_auth/components/layouts/forgot_password";
import { ResetPasswordLayout } from "hazo_auth/components/layouts/reset_password";
import { EmailVerificationLayout } from "hazo_auth/components/layouts/email_verification";
import { MySettingsLayout } from "hazo_auth/components/layouts/my_settings";
import { UserManagementLayout } from "hazo_auth/components/layouts/user_management";
import { RbacTestLayout } from "hazo_auth/components/layouts/rbac_test";

// Shared layout components and hooks (barrel import - recommended)
import { 
  ProfilePicMenu,
  ProfilePicMenuWrapper,
  FormActionButtons,
  use_hazo_auth,
  use_auth_status 
} from "hazo_auth/components/layouts/shared";

// Server-side authentication utility
import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";

// Server utilities
import { ... } from "hazo_auth/server";

// Edge-compatible proxy/middleware authentication (v1.6.6+)
import { validate_session_cookie } from "hazo_auth/server/middleware";

Note: The package uses relative imports internally. Consumers should only import from the exposed entry points listed above. Do not import from internal paths like hazo_auth/components/ui/* - these are internal modules.

Using Layout Components

Prefer to drop the forms into your own routes without using the pre-built pages? Import the layouts directly and feed them a data_client plus any label/button overrides:

// app/(auth)/login/page.tsx in your project
import { LoginLayout, createLayoutDataClient } from "hazo_auth";
import { create_postgrest_hazo_connect } from "hazo_auth/lib/hazo_connect_setup";

export default async function LoginPage() {
  const hazoConnect = create_postgrest_hazo_connect();
  const dataClient = createLayoutDataClient(hazoConnect);

  return (
    <div className="my-app-shell">
      <LoginLayout
        image_src="/marketing/login-hero.svg"
        image_alt="Login hero image"
        data_client={dataClient}
        redirectRoute="/dashboard"
      />
    </div>
  );
}

Available Layout Components:

  • LoginLayout - Login form with email/password
  • RegisterLayout - Registration form with password requirements and OAuth (Google Sign-In)
  • ForgotPasswordLayout - Request password reset
  • ResetPasswordLayout - Set new password with token
  • EmailVerificationLayout - Verify email address
  • MySettingsLayout - User profile and settings
  • UserManagementLayout - Admin user/role management (requires user_management API routes)
  • RbacTestLayout - RBAC/HRBAC permission and scope testing tool (requires admin_test_access permission)

User Management Component

The UserManagementLayout component provides a comprehensive admin interface for managing users, roles, and permissions. It requires the user_management API routes to be set up in your project.

Required Permissions:

  • admin_user_management - Access to Users tab
  • admin_role_management - Access to Roles tab
  • admin_permission_management - Access to Permissions tab
  • admin_scope_hierarchy_management - Access to Scope Hierarchy tab (HRBAC)
  • admin_system - Access to Scope Labels tab (HRBAC)
  • admin_user_scope_assignment - Access to User Scopes tab (HRBAC)

Required API Routes: The UserManagementLayout component requires the following API routes to be created in your project:

// app/api/hazo_auth/user_management/users/route.ts
export { GET, PATCH, POST } from "hazo_auth/server/routes";

// app/api/hazo_auth/user_management/permissions/route.ts
export { GET, POST, PUT, DELETE } from "hazo_auth/server/routes";

// app/api/hazo_auth/user_management/roles/route.ts
export { GET, POST, PUT } from "hazo_auth/server/routes";

// app/api/hazo_auth/user_management/users/roles/route.ts
export { GET, POST, PUT } from "hazo_auth/server/routes";

Note: These routes are automatically created when you run npx hazo_auth generate-routes. The routes handle:

  • Users: List users, deactivate users, send password reset emails
  • Permissions: List permissions (from DB and config), migrate config permissions to DB, create/update/delete permissions
  • Roles: List roles with permissions, create roles, update role-permission assignments
    • UI Enhancement: The Roles tab uses a tag-based UI for better readability. Each role displays permissions as inline tags/chips (showing up to 4, with "+N more" to expand). Edit permissions via an interactive dialog with Select All/Unselect All buttons.
  • User Roles: Get user roles, assign roles to users, bulk update user role assignments

Organization Management Component

The OrgManagementLayout component provides an admin interface for managing the organization hierarchy when multi-tenancy is enabled. It requires the org_management API routes to be set up in your project.

Required Permissions:

  • hazo_perm_org_management - CRUD operations on organizations
  • hazo_org_global_admin - View/manage all organizations across the system (optional, for global admins)

Required API Routes: The OrgManagementLayout component requires the following API route to be created in your project:

// app/api/hazo_auth/org_management/orgs/route.ts
export {
  orgManagementOrgsGET as GET,
  orgManagementOrgsPOST as POST,
  orgManagementOrgsPATCH as PATCH,
  orgManagementOrgsDELETE as DELETE
} from "hazo_auth/server/routes";

Note: This route is automatically created when you run npx hazo_auth generate-routes. The route handles:

  • GET: List organizations (with action=tree query parameter for hierarchical tree structure)
  • POST: Create new organization
  • PATCH: Update existing organization (name, user_limit, active status)
  • DELETE: Soft delete organization (sets active=false, does not remove from database)

Example Usage:

// app/hazo_auth/user_management/page.tsx
import { UserManagementLayout } from "hazo_auth/components/layouts/user_management";

export default function UserManagementPage() {
  return <UserManagementLayout />;
}

The component automatically shows/hides tabs based on the user's permissions, so users will only see the tabs they have access to.

Shared Components:

  • ProfilePicMenu / ProfilePicMenuWrapper - Navbar profile menu
  • FormActionButtons, FormFieldWrapper, PasswordField
  • And more under hazo_auth/components/layouts/shared/

Choose the UI Shell (Test Sidebar vs Standalone)

By default, the pages render inside the "test workspace" sidebar so you can quickly preview every flow. When you reuse the routes inside another project you'll usually want a clean, standalone wrapper instead. Set this in hazo_auth_config.ini:

[hazo_auth__ui_shell]
# Options: test_sidebar | standalone
layout_mode = standalone
vertical_center = auto  # 'auto' enables vertical centering when navbar is present
# Optional tweaks for the standalone header wrapper/classes:
# standalone_heading = Welcome back
# standalone_description = Your description here
# standalone_wrapper_class = min-h-screen bg-background py-8
# standalone_content_class = mx-auto w-full max-w-4xl rounded-2xl border bg-card
  • test_sidebar: keeps the developer sidebar (perfect for the demo workspace or Storybook screenshots).
  • standalone: renders the page body directly so it inherits your own app shell, layout, and theme tokens.
  • vertical_center: controls vertical centering of auth content (auto enables centering when navbar is present)
  • The wrapper and content class overrides let you align spacing/borders with your design system without editing package code.

Authentication Page Navbar

The navbar now works automatically - zero-config server page components include the navbar based on configuration without manual wrapping.

When using layout_mode = standalone, you can enable a configurable navbar that appears on all auth pages:

[hazo_auth__navbar]
enable_navbar = true              # Show navbar on auth pages
logo_path = /logo.png             # Path to logo image
logo_width = 32                   # Logo width in pixels
logo_height = 32                  # Logo height in pixels
company_name = My Company         # Company name (links to home)
home_path = /                     # URL for logo and company name link
home_label = Home                 # Label for home link
show_home_link = true             # Show "Home" link on right side
background_color =                # Custom background (optional)
text_color =                      # Custom text color (optional)
height = 64                       # Navbar height in pixels

The navbar provides consistent branding across authentication pages with your company logo, name, and optional home link. It automatically vertically centers auth content when enabled.

Zero-config usage (recommended):

// app/hazo_auth/login/page.tsx
import { LoginPage } from "hazo_auth/pages/login";

export default function Page() {
  return <LoginPage />;  // Navbar appears automatically if enabled in config
}

Customize via props (advanced):

import { LoginLayout } from "hazo_auth/components/layouts/login";

export default function Page() {
  return (
    <LoginLayout
      navbar={{
        logo_path: "/custom-logo.svg",
        company_name: "Acme Corp",
        background_color: "#1a1a1a",
      }}
    />
  );
}

Disable for specific pages:

<LoginPage disableNavbar={true} />
// OR for layout components:
<LoginLayout navbar={{ enable_navbar: false }} />

Authentication Service

The hazo_auth package provides a comprehensive authentication and authorization system with role-based access control (RBAC). The main authentication utilities are:

  • hazo_get_auth - Standard authentication with user details, permissions, and caching
  • hazo_get_tenant_auth - Tenant-aware authentication that extracts scope context from request headers or cookies
  • require_tenant_auth - Strict tenant authentication with typed error handling

These utilities provide user details, permissions, and permission checking with built-in caching and rate limiting.

Client-Side API Endpoint (Recommended)

/api/hazo_auth/me (GET) - Standardized User Info Endpoint

⚠️ IMPORTANT: Use this endpoint for all client-side authentication checks. It always returns the same standardized format with permissions.

This is the standardized endpoint that ensures consistent response format across all projects. It always includes permissions and user information in a unified structure.

Endpoint: GET /api/hazo_auth/me

Response Format (Authenticated):

{
  authenticated: true,
  // Top-level fields (for backward compatibility)
  user_id: string,
  email: string,
  name: string | null,
  email_verified: boolean,
  last_logon: string | undefined,
  profile_picture_url: string | null,
  profile_source: "upload" | "library" | "gravatar" | "custom" | undefined,
  // Profile picture aliases (for consuming app compatibility)
  profile_image?: string,  // Alias for profile_picture_url
  avatar_url?: string,     // Alias for profile_picture_url
  image?: string,          // Alias for profile_picture_url
  // Permissions (always included)
  user: {
    id: string,
    email_address: string,
    name: string | null,
    is_active: boolean,
    profile_picture_url: string | null,
  },
  permissions: string[],
  permission_ok: boolean,
  missing_permissions?: string[],
}

Response Format (Not Authenticated):

{
  authenticated: false
}

Example Usage:

// Client-side (React component)
const response = await fetch("/api/hazo_auth/me", {
  method: "GET",
  credentials: "include",
});

const data = await response.json();

if (data.authenticated) {
  console.log("User:", data.user);
  console.log("Email:", data.email);
  console.log("Permissions:", data.permissions);
  console.log("Permission OK:", data.permission_ok);
}

Why Use /api/hazo_auth/me?

  • Standardized format - Always returns the same structure
  • Always includes permissions - No need for separate permission checks
  • Backward compatible - Top-level fields work with existing code
  • Single source of truth - Prevents downstream variations

Note: The use_auth_status hook automatically uses this endpoint and includes permissions in its return value.

Proxy/Middleware Authentication

hazo_auth provides Edge-compatible authentication for Next.js proxy/middleware files. Note: Next.js is migrating from middleware.ts to proxy.ts (see Next.js documentation), but the functionality remains the same.

Edge Runtime Limitations

Next.js proxy/middleware runs in Edge Runtime, which cannot use Node.js APIs (like SQLite). Therefore, hazo_get_auth cannot be used directly in proxy/middleware because it requires database access.

JWT Session Tokens

New in v1.6.6+: hazo_auth now issues JWT session tokens on login that can be validated in Edge Runtime:

  • Cookie Name: hazo_auth_session
  • Token Type: JWT (signed with JWT_SECRET)
  • Expiry: 30 days (configurable)
  • Validation: Signature and expiry checked without database access
  • Backward Compatible: Existing hazo_auth_user_id and hazo_auth_user_email cookies still work

Requirements:

  • JWT_SECRET environment variable must be set (see Configuration Setup)
  • The jose package is included as a dependency (Edge-compatible JWT library)

Using in Proxy/Middleware

Recommended: Use JWT validation (Edge-compatible)

// proxy.ts (or middleware.ts - both work)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { validate_session_cookie } from "hazo_auth/server/middleware";

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Protect routes
  if (pathname.startsWith("/members")) {
    const { valid } = await validate_session_cookie(request);
    
    if (!valid) {
      const login_url = new URL("/hazo_auth/login", request.url);
      login_url.searchParams.set("redirect", pathname);
      return NextResponse.redirect(login_url);
    }
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Fallback: Simple cookie check (less secure, but works)

If JWT validation fails or you need a simpler check:

// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  if (pathname.startsWith("/members")) {
    const user_id = request.cookies.get("hazo_auth_user_id")?.value;
    const user_email = request.cookies.get("hazo_auth_user_email")?.value;
    
    if (!user_id || !user_email) {
      const login_url = new URL("/hazo_auth/login", request.url);
      login_url.searchParams.set("redirect", pathname);
      return NextResponse.redirect(login_url);
    }
  }
  
  return NextResponse.next();
}

Important Notes:

  • JWT validation provides better security (signature validation, tamper detection)
  • Simple cookie check is faster but doesn't validate token integrity
  • Full user status checks (e.g., deactivated accounts) happen in API routes/layouts
  • Both approaches work - JWT is recommended for production

Server-Side Functions

hazo_get_tenant_auth (Recommended for Multi-Tenant Apps)

New: Tenant-aware authentication function that extracts scope context from request headers or cookies and returns enriched result with organization information.

Location: src/lib/auth/hazo_get_tenant_auth.server.ts

Scope Context Extraction:

  • Header (priority): X-Hazo-Scope-Id (configurable via scope_header_name)
  • Cookie (fallback): hazo_auth_scope_id (with prefix if configured)

Function Signature:

import { hazo_get_tenant_auth } from "hazo_auth";
import type { TenantAuthResult, TenantAuthOptions } from "hazo_auth";

async function hazo_get_tenant_auth(
  request: NextRequest,
  options?: TenantAuthOptions
): Promise<TenantAuthResult>

Options:

  • required_permissions?: string[] - Array of permission names to check
  • strict?: boolean - If true, throws errors when checks fail (default: false)
  • scope_header_name?: string - Custom header name for scope ID (default: "X-Hazo-Scope-Id")
  • scope_cookie_name?: string - Custom cookie name for scope ID (default: "hazo_auth_scope_id")

Return Type:

type TenantAuthResult =
  | {
      authenticated: true;
      user: HazoAuthUser;
      permissions: string[];
      permission_ok: boolean;
      missing_permissions?: string[];
      organization: TenantOrganization | null;  // NEW: Tenant context
      user_scopes: ScopeDetails[];              // NEW: All user's scopes for switching
      scope_ok: boolean;
    }
  | {
      authenticated: false;
      user: null;
      permissions: [];
      permission_ok: false;
      organization: null;
      user_scopes: [];
      scope_ok: false;
    };

type TenantOrganization = {
  id: string;
  name: string;
  slug: string | null;          // URL-friendly identifier
  level: string;                // "Company", "Division", etc.
  role_id: string;              // User's role in this scope
  is_super_admin: boolean;
  branding?: {
    logo_url: string | null;
    primary_color: string | null;
    secondary_color: string | null;
    tagline: string | null;
  };
};

type ScopeDetails = {
  id: string;
  name: string;
  slug: string | null;
  level: string;
  parent_id: string | null;
  role_id: string;
  logo_url: string | null;
  primary_color: string | null;
  secondary_color: string | null;
  tagline: string | null;
};

Basic Usage Example:

// app/api/dashboard/route.ts
import { NextRequest, NextResponse } from "next/server";
import { hazo_get_tenant_auth } from "hazo_auth";

export async function GET(request: NextRequest) {
  const auth = await hazo_get_tenant_auth(request);

  if (!auth.authenticated) {
    return NextResponse.json({ error: "Authentication required" }, { status: 401 });
  }

  if (!auth.organization) {
    return NextResponse.json(
      {
        error: "No organization context",
        available_scopes: auth.user_scopes.map(s => ({ id: s.id, name: s.name }))
      },
      { status: 403 }
    );
  }

  // Access tenant-specific data
  const data = await getTenantData(auth.organization.id);

  return NextResponse.json({
    organization: auth.organization,
    data,
    // Include available scopes for UI scope switcher
    available_scopes: auth.user_scopes,
  });
}

Strict Mode with Error Handling:

import { require_tenant_auth, HazoAuthError } from "hazo_auth";

export async function GET(request: NextRequest) {
  try {
    const auth = await require_tenant_auth(request, {
      required_permissions: ["view_reports"],
    });

    // auth.organization is guaranteed non-null here
    const reports = await getReports(auth.organization.id);
    return NextResponse.json({ reports });
  } catch (error) {
    if (error instanceof HazoAuthError) {
      return NextResponse.json(
        {
          error: error.message,
          code: error.code,
          // For TenantAccessDeniedError, includes available_scopes
          available_scopes: error.available_scopes
        },
        { status: error.status_code }
      );
    }
    throw error;
  }
}

Frontend Integration:

// Client sets scope via header or cookie
const response = await fetch("/api/dashboard", {
  headers: {
    "X-Hazo-Scope-Id": selectedScopeId,
  },
});

// Or via cookie (set once during scope selection)
document.cookie = `hazo_auth_scope_id=${selectedScopeId}; path=/`;

Error Types:

import {
  AuthenticationRequiredError,   // 401 - User not authenticated
  TenantRequiredError,            // 403 - No tenant context provided
  TenantAccessDeniedError,        // 403 - User lacks access to tenant
} from "hazo_auth";

require_tenant_auth (Strict Tenant Auth)

Helper function that wraps hazo_get_tenant_auth and throws typed errors for common failure cases.

Throws:

  • AuthenticationRequiredError (401) - User not authenticated
  • TenantRequiredError (403) - No tenant context in request
  • TenantAccessDeniedError (403) - User lacks access to requested tenant

Returns: RequiredTenantAuthResult with guaranteed non-null organization

Example:

export async function GET(request: NextRequest) {
  try {
    // organization is guaranteed to exist
    const { organization, user, permissions } = await require_tenant_auth(request);

    const data = await getData(organization.id);
    return NextResponse.json(data);
  } catch (error) {
    if (error instanceof HazoAuthEr