npm package: @kodeme-io/next-core-auth
Description: Complete authentication solution for Next.js + Odoo applications with Odoo native auth, OIDC/Authentik, PKCE, and protected route middleware
Author: ABC Food
Latest version: 0.8.4

# @next-odoo/auth

Authentication module for Next.js + Odoo applications with support for Odoo native authentication and OIDC (Authentik/Keycloak).

## Features

- ✅ **Dual Authentication** - Odoo native + OIDC support
- ✅ **Persistent Sessions** - Automatic session persistence with Zustand
- ✅ **Type Safety** - Full TypeScript support
- ✅ **Operating Units** - Multi-branch/region support
- ✅ **React Hook** - Easy-to-use `useAuth()` hook
- ✅ **Salesman Types** - Canvas, Taking Order, or Both
- ✅ **SSR Safe** - Works with Next.js server-side rendering

## Installation

```bash
pnpm add @next-odoo/auth
```

## Usage

### Basic Setup

```typescript
import { useAuth } from '@next-odoo/auth'

export function LoginPage() {
  const { login, isLoading } = useAuth()

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)

    const success = await login({
      username: formData.get('username') as string,
      password: formData.get('password') as string,
    })

    if (success) {
      // Redirect to dashboard
      router.push('/dashboard')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" placeholder="Username" />
      <input name="password" type="password" placeholder="Password" />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  )
}
```

### Protected Routes

```typescript
import { useAuth } from '@next-odoo/auth'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export function ProtectedLayout({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, isLoading, user } = useAuth()
  const router = useRouter()

  useEffect(() => {
    if (!isLoading && !isAuthenticated) {
      router.push('/login')
    }
  }, [isLoading, isAuthenticated, router])

  if (isLoading) {
    return <div>Loading...</div>
  }

  if (!isAuthenticated) {
    return null
  }

  return <>{children}</>
}
```

### Access User Information

```typescript
import { useAuth } from '@next-odoo/auth'

export function UserProfile() {
  const { user, logout } = useAuth()

  if (!user) return null

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Login: {user.login}</p>

      {user.salesman_type && (
        <p>Salesman Type: {user.salesman_type}</p>
      )}

      {user.default_operating_unit_name && (
        <p>Operating Unit: {user.default_operating_unit_name}</p>
      )}

      <button onClick={logout}>Logout</button>
    </div>
  )
}
```

### Check Permissions

```typescript
import { useAuth } from '@next-odoo/auth'

export function CustomerApprovalButton() {
  const { user } = useAuth()

  if (!user?.can_approve_customers) {
    return null // Don't show button if user can't approve
  }

  return (
    <button onClick={handleApprove}>
      Approve Customer
    </button>
  )
}
```

## API Reference

### `useAuth()`

Main hook for authentication functionality.

**Returns:**

```typescript
{
  user: User | null              // Current authenticated user
  sessionId: string | null       // Odoo session ID
  isAuthenticated: boolean       // Authentication status
  isLoading: boolean            // Loading state
  login: (credentials: LoginCredentials) => Promise<boolean>
  logout: () => Promise<void>
  checkSession: () => Promise<boolean>
}
```

### Types

#### `User`

```typescript
interface User {
  id: number
  name: string
  login: string
  email: string
  is_salesperson: boolean
  partner_id: number
  salesman_type?: 'canvas' | 'taking_order' | 'both'
  can_approve_customers?: boolean
  operating_unit_ids?: number[]
  default_operating_unit_id?: number
  default_operating_unit_name?: string
  sale_team_id?: number
  sale_team_name?: string
}
```

#### `LoginCredentials`

```typescript
interface LoginCredentials {
  username: string
  password: string
}
```

## Backend Integration

This package expects your Next.js app to have these API routes:

### `/api/auth/login` (POST)

```typescript
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const { username, password } = await request.json()

  // Call Odoo JSON-RPC to authenticate
  const response = await fetch(`${process.env.ODOO_URL}/jsonrpc`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'call',
      params: {
        service: 'common',
        method: 'login',
        args: [process.env.ODOO_DB, username, password]
      }
    })
  })

  const result = await response.json()

  if (result.result) {
    return NextResponse.json({
      success: true,
      sessionId: result.result.session_id,
      user: {
        id: result.result.uid,
        // ... other user fields
      }
    })
  }

  return NextResponse.json({ success: false }, { status: 401 })
}
```

### `/api/auth/logout` (POST)

```typescript
// app/api/auth/logout/route.ts
export async function POST() {
  // Clear session cookies/tokens
  return NextResponse.json({ success: true })
}
```

## OIDC Integration (Optional)

For OIDC authentication with Authentik/Keycloak:

```typescript
import { OIDCConfig } from '@next-odoo/auth'

const oidcConfig: OIDCConfig = {
  issuer: 'https://auth.yourdomain.com',
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  redirectUri: 'https://yourapp.com/api/auth/callback',
  scopes: ['openid', 'profile', 'email']
}

// Implement OIDC flow in your API routes
```

## Storage Configuration

By default, auth state is persisted to `localStorage` with the key `next-odoo-auth-storage`. To customize:

```typescript
import { useAuthStore } from '@next-odoo/auth'

// Access the store directly for advanced configuration
const authStore = useAuthStore.getState()
```

## License

MIT