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

odoo-nextjs

v0.1.1

Published

Intégration Odoo 16 (JSON-RPC) pour Next.js 13+ : auth, sessions, permissions, middleware et hooks React.

Downloads

153

Readme

odoo-nextjs

CI TypeScript Next.js License: MIT

Package TypeScript zéro-dépendance runtime pour intégrer un backend Odoo 16 (JSON-RPC) à un frontend Next.js 13+ (App Router). Couvre l'authentification, la gestion de session, les permissions utilisateur (ACL + groupes), la protection des API Routes et de l'Edge middleware, et des hooks React prêts à l'emploi.


Sommaire

  1. Pourquoi ce package ?
  2. Installation
  3. Configuration initiale
  4. Guide d'utilisation
  5. Exemples
  6. Référence des erreurs
  7. Documentation

Pourquoi ce package ?

| Problème | Solution odoo-nextjs | |----------|------------------------| | Auth Odoo redondante entre API Routes | withOdooAuth injecte { session, user } | | Vérification d'ACL / groupe dupliquée | withOdooPermissions centralise | | Logique de session dans le code applicatif | SessionManager + MemorySessionStore (Redis-ready) | | Cache des permissions fait maison | loadUserPermissions met en cache automatiquement | | Mixité client / serveur sans typage strict | 3 points d'entrée séparés (odoo-nextjs, /react, /middleware) | | Pas de protection réseau | createOdooMiddleware (Edge) + createRateLimiter |

Le package n'a aucune dépendance runtime : il n'utilise que fetch, node:crypto et l'API standard Web Crypto. Compatible Edge runtime.


Installation

npm install odoo-nextjs

next (≥ 13) et react (≥ 18) doivent être présents en peer dependencies dans ton package.json.


Configuration initiale

Crée un fichier lib/odoo.ts à la racine de ton projet Next.js. C'est le seul endroit où configureOdoo() doit être appelé :

// lib/odoo.ts
import { configureOdoo } from "odoo-nextjs";

// Astuce : garde ce module idempotent en le marquant côté serveur.
// → dans une app Next.js, importe-le dans chaque API route :
//   import "../../lib/odoo.js";
// Next le charge une seule fois par instance.

configureOdoo({
  // ── Obligatoire ─────────────────────────────────────────
  url: process.env.ODOO_URL!,                  // ex: https://odoo.example.com
  database: process.env.ODOO_DB!,              // nom de la base Odoo
  serviceAccount: {
    login: process.env.ODOO_LOGIN!,            // login technique
    password: process.env.ODOO_PASSWORD!,      // mot de passe technique
  },

  // ── Optionnel ───────────────────────────────────────────
  session: {
    cacheTTL: 6 * 60 * 60 * 1000,              // 6 h par défaut
    maxRetries: 2,                              // tentatives de re-auth
    encryptionKey: process.env.ODOO_ENCRYPTION_KEY, // pour chiffrer les mots
                                                   // de passe en session store
  },
  permissions: {
    cacheTTL: 5 * 60 * 1000,                   // 5 min par défaut
    models: [                                  // modèles dont tu veux
      "sale.order",                            // précharger les ACL au login
      "product.product",
      "res.partner",
    ],
  },
});

Variables d'environnement recommandées (.env.local) :

ODOO_URL=https://odoo.example.com
ODOO_DB=production
[email protected]
ODOO_PASSWORD=changeme
ODOO_ENCRYPTION_KEY=base64:ta-clé-de-32-octets-en-base64

⚠️ Sécurité : ODOO_ENCRYPTION_KEY doit faire 32 octets minimum. Elle sert à chiffrer les mots de passe utilisateurs en MemorySessionStore. Sans elle, les sessions ne peuvent pas être renouvelées automatiquement par renewFromStore.


Guide d'utilisation

1. Protéger une API Route par session

Le wrapper withOdooAuth extrait le cookie de session, le valide (renouvellement automatique via SessionManager si expiré), puis appelle ton handler avec { session, user }.

// app/api/odoo/orders/route.ts
import { withOdooAuth } from "odoo-nextjs/middleware";
import { OdooClient } from "odoo-nextjs";

export const GET = withOdooAuth(async (_req, { session, user }) => {
  // `session.cookie` est un cookie Odoo valide (renouvelé si besoin).
  // `user` contient uid, name, login, groups, userType, etc.
  const orders = await OdooClient.searchRead(
    "sale.order",
    {
      domain: [["state", "!=", "cancel"]],
      fields: ["name", "amount_total", "state"],
      limit: 50,
      order: "date_order desc",
    },
    { sessionCookie: session.cookie },
  );

  return Response.json({ orders, user });
});

Réponses d'erreur émises automatiquement par le wrapper :

| Status | Code | Cas | |--------|-------------------|--------------------------------------------------| | 401 | unauthenticated | cookie absent | | 401 | session_expired | session invalide | | 503 | odoo_error | erreur Odoo remontée (timeout, etc.) | | 500 | internal_error | exception non gérée |


2. Protéger une API Route par permissions Odoo

withOdooPermissions vérifie en plus un droit CRUD ou un groupe. Charge automatiquement les permissions via loadUserPermissions (avec cache).

// app/api/odoo/invoices/route.ts
import { withOdooPermissions } from "odoo-nextjs/middleware";
import { OdooClient } from "odoo-nextjs";

export const POST = withOdooPermissions(
  {
    model: "account.move",                  // vérification `canDo(model, operation)`
    operation: "create",
    requiredGroup: "Invoicing / User",      // OU vérification `hasGroup(group)`
    allowedUserTypes: ["internal", "admin"], // restreint aux types d'utilisateurs
  },
  async (req, { session, user, permissions }) => {
    const body = await req.json();
    const id = await OdooClient.create("account.move", body, {
      sessionCookie: session.cookie,
    });
    return Response.json({ id }, { status: 201 });
  },
);

{ session, user, permissions } est le contexte enrichi. permissions contient { user, modelAccess, cachedAt }.


3. Lire / écrire des données via OdooClient

Le client RPC supporte les méthodes ORM usuelles d'Odoo. Toutes les méthodes lancent des erreurs typées en cas de problème (voir Référence des erreurs).

import { OdooClient } from "odoo-nextjs";

// Lire
const partners = await OdooClient.searchRead<{ id: number; name: string }>(
  "res.partner",
  {
    domain: [["is_company", "=", true]],
    fields: ["name", "email"],
    limit: 20,
    order: "name asc",
  },
  { sessionCookie: session.cookie },
);

// Compter
const total = await OdooClient.searchCount(
  "res.partner",
  { domain: [["is_company", "=", true]] },
  { sessionCookie: session.cookie },
);

// Créer
const orderId = await OdooClient.create(
  "sale.order",
  { partner_id: 42 },
  { sessionCookie: session.cookie },
);

// Mettre à jour
await OdooClient.write("res.partner", [42], { name: "Nouveau nom" }, {
  sessionCookie: session.cookie,
});

// Supprimer
await OdooClient.unlink("res.partner", [42], { sessionCookie: session.cookie });

// Méthode custom
const result = await OdooClient.call<{ ok: boolean }>(
  "sale.order",
  "action_confirm",
  [[orderId]],
  {},
  { sessionCookie: session.cookie },
);

// Avec timeout (AbortController)
const products = await OdooClient.searchRead(
  "product.product",
  { limit: 10 },
  { sessionCookie: session.cookie, timeout: 5_000 },
);

💡 Le sessionCookie est obligatoire pour les actions au nom d'un utilisateur. Sans lui, OdooClient utilise le serviceAccount configuré (mode admin système).


4. Construire des domaines Odoo typés

DomainBuilder offre une API fluent pour construire des domaines Odoo en évitant les erreurs de manipulation de tuples bruts. Les helpers condition/andDomain/orDomain/notDomain/fieldSelector sont aussi disponibles pour des cas simples.

import { DomainBuilder, fieldSelector, condition, orDomain } from "odoo-nextjs";

// Avec le builder fluent
const domain = new DomainBuilder()
  .where("sale_ok", "=", true)
  .where("list_price", ">", 10)
  .or((b) => b
    .where("name", "ilike", "electronics")
    .where("name", "ilike", "furniture"),
  )
  .build();
// [
//   ["sale_ok", "=", true],
//   ["list_price", ">", 10],
//   "|",
//   ["name", "ilike", "electronics"],
//   ["name", "ilike", "furniture"],
// ]

// Avec les helpers
const fields = fieldSelector("name", "list_price", "name");
// ["name", "list_price"]   ← dédupliqué, ordre préservé

const simpleCondition = condition("state", "=", "draft");
// ["state", "=", "draft"]

const orDomain1 = orDomain(
  [["name", "ilike", "a"]],
  [["name", "ilike", "b"]],
);
// ["|", ["name", "ilike", "a"], ["name", "ilike", "b"]]

5. Authentifier un utilisateur côté serveur

authenticate est typiquement appelé par la route /api/odoo/login :

// app/api/odoo/login/route.ts
import { NextResponse } from "next/server";
import { authenticate } from "odoo-nextjs";

export async function POST(req: Request) {
  const { email, password } = await req.json();
  try {
    const session = await authenticate(email, password);
    // session = { uid, cookie, user: OdooUser }
    const response = NextResponse.json({ user: session.user });
    response.cookies.set("odoo_session", session.cookie, {
      httpOnly: true,
      sameSite: "lax",
      secure: process.env.NODE_ENV === "production",
      path: "/",
    });
    return response;
  } catch {
    return NextResponse.json(
      { error: { code: "unauthenticated", message: "Identifiants invalides." } },
      { status: 401 },
    );
  }
}

Pour les déploiements multi-instance, implémente un SessionStore Redis :

// lib/redis-session-store.ts
import type { SessionStore, StoredSession } from "odoo-nextjs";
import { redis } from "./redis";

export class RedisSessionStore implements SessionStore {
  async get(key: string): Promise<StoredSession | null> {
    const raw = await redis.get(`odoo:session:${key}`);
    return raw ? (JSON.parse(raw) as StoredSession) : null;
  }
  async set(key: string, session: StoredSession, ttl: number): Promise<void> {
    await redis.set(
      `odoo:session:${key}`,
      JSON.stringify(session),
      ttl > 0 ? { EX: Math.floor(ttl / 1000) } : undefined,
    );
  }
  async delete(key: string): Promise<void> {
    await redis.del(`odoo:session:${key}`);
  }
}

// Injection dans le wrapper (via lib/odoo.ts)
import { SessionManager } from "odoo-nextjs";
import { RedisSessionStore } from "./redis-session-store";

export const sessionManager = new SessionManager({
  store: new RedisSessionStore(),
});

6. Configurer le OdooProvider côté React

Le provider principal est la source de vérité pour l'utilisateur courant et expose login, logout, request.

// app/layout.tsx
import { OdooProvider, PermissionsProvider } from "odoo-nextjs/react";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr">
      <body>
        <OdooProvider
          apiBasePath="/api/odoo"
          loginPath="/login"
          onSessionExpired={() => console.warn("Session expirée")}
        >
          <PermissionsProvider
            refreshInterval={5 * 60 * 1000}
            models={["sale.order", "product.product", "res.partner"]}
          >
            {children}
          </PermissionsProvider>
        </OdooProvider>
      </body>
    </html>
  );
}

⚠️ OdooProvider doit être placé sous app/layout.tsx. Les pages App Router qui consomment useOdoo() ou usePermissions() doivent porter la directive "use client".

Le PermissionsProvider charge automatiquement les groupes + ACL via GET /api/odoo/permissions et les rafraîchit toutes les refreshInterval ms. Tu peux aussi appeler refresh() à la demande.


7. Fetch + cache via useOdooQuery

useOdooQuery est un wrapper typé autour de fetch qui gère le loading, le retry, le polling et les erreurs.

"use client";
import { useOdooQuery } from "odoo-nextjs/react";

type Product = { id: number; name: string; list_price: number };
type ProductsResponse = { products: Product[] };

function ProductList() {
  const { data, loading, error, refetch } = useOdooQuery<ProductsResponse>(
    "/products",
    {
      params: { limit: 20, offset: 0 },
      refreshInterval: 30_000,   // polling toutes les 30 s
      retryOnError: true,
      maxRetries: 2,
    },
  );

  if (loading) return <p>Chargement…</p>;
  if (error) return <p style={{ color: "red" }}>Erreur : {error.message}</p>;
  if (!data) return null;

  return (
    <ul>
      {data.products.map((p) => (
        <li key={p.id}>{p.name} — {p.list_price} €</li>
      ))}
    </ul>
  );
}

Pour les URLs absolues (CDN, autre service), useOdooQuery ne préfixe pas avec apiBasePath :

useOdooQuery("https://other.example.com/data");

8. Garde de rendu côté React

Trois composants pour conditionner le rendu :

import {
  RequireGroup,
  RequireAccess,
  ProtectedPage,
} from "odoo-nextjs/react";

// Affiche children si l'utilisateur a le groupe, sinon fallback
<RequireGroup group="Sales / Manager" fallback={<p>Accès restreint</p>}>
  <AdminPanel />
</RequireGroup>

// Affiche children si l'utilisateur a l'ACL `operation` sur `model`
<RequireAccess model="sale.order" operation="create" fallback={null}>
  <CreateOrderButton />
</RequireAccess>

// Protège une page entière : redirige si pas autorisé
<ProtectedPage
  requiredGroup="Sales"
  requiredModel="sale.order"
  requiredOperation="read"
  redirectTo="/unauthorized"
  loadingComponent={<Spinner />}
>
  <SalesPage />
</ProtectedPage>

Pour la logique conditionnelle dans ton code, utilise le hook usePermissions() :

"use client";
import { usePermissions } from "odoo-nextjs/react";

function OrderActions() {
  const { hasGroup, canDo, user, loading } = usePermissions();
  if (loading) return null;
  if (!user) return <p>Non authentifié</p>;

  return (
    <div>
      {canDo("sale.order", "create") && <button>Créer une commande</button>}
      {hasGroup("Sales / Manager") && <a href="/admin">Admin</a>}
    </div>
  );
}

9. Middleware Edge (couche réseau)

createOdooMiddleware est un middleware Next.js Edge-compatible qui s'exécute avant tes API Routes. Il protège des patterns de routes et redirige les non-authentifiés.

// middleware.ts (à la racine du projet Next.js)
import { createOdooMiddleware } from "odoo-nextjs/middleware";

export default createOdooMiddleware({
  protectedRoutes: ["/api/odoo/:path*"],
  publicRoutes: ["/api/odoo/login", "/api/odoo/health"],
  loginRedirect: "/login",
  onSessionExpired: "redirect",        // ou "json-error" pour renvoyer 401
  sessionCookieName: "odoo_session",   // nom du cookie (défaut)
});

export const config = {
  matcher: ["/api/odoo/:path*"],
};

💡 Le middleware Edge est une barrière réseau légère (vérifie juste la présence du cookie). La vérification de session réelle est faite par withOdooAuth côté API Route.


10. Rate limiter

createRateLimiter retourne un limiteur en mémoire partageable entre routes :

// app/api/odoo/_limiter.ts
import { createRateLimiter } from "odoo-nextjs/middleware";

export const limiter = createRateLimiter({
  windowMs: 60_000,                     // 1 minute
  maxRequests: 100,                      // 100 req/min
  keyExtractor: (req) =>
    req.cookies.get("odoo_session")?.value ?? "anonymous",
});
// app/api/odoo/products/route.ts
import { limiter } from "../_limiter";
import { withOdooAuth, OdooClient } from "odoo-nextjs";
import { withOdooAuth as _withAuth } from "odoo-nextjs/middleware";

export const GET = _withAuth(async (req, { session }) => {
  const verdict = limiter.check(req);
  if (!verdict.allowed) {
    return Response.json(
      { error: { code: "rate_limited" } },
      { status: 429, headers: { "Retry-After": String(verdict.retryAfter ?? 60) } },
    );
  }
  const products = await OdooClient.searchRead(
    "product.product",
    { fields: ["name", "list_price"], limit: 50 },
    { sessionCookie: session.cookie },
  );
  return Response.json({ products });
});

Pour un environnement multi-instance, implémente ton propre store (Redis recommandé) — l'interface publique est documentée dans CLAUDE.md.


11. Pagination

Trois helpers pour convertir entre page/pageSize (côté front) et offset/limit (côté Odoo) :

import {
  toOdooPagination,
  fromOdooPagination,
  normalizePagination,
  paginationWindow,
} from "odoo-nextjs";

// Front → Odoo
const { offset, limit } = toOdooPagination({ page: 3, pageSize: 20 });
// { offset: 40, limit: 20 }

// Odoo → Front (avec total)
const meta = fromOdooPagination({ offset: 40, limit: 20, total: 150 });
// {
//   page: 3, pageSize: 20, total: 150, totalPages: 8,
//   hasNext: true, hasPrevious: true,
// }

// Normalisation (page < 1 → 1, pageSize > 1000 → 1000)
const { page, pageSize } = normalizePagination(req.query);

// Fenêtre de pagination (affiche 5 pages autour de la page courante)
const { start, end } = paginationWindow({ page: 5, totalPages: 20 }, 5);
// { start: 3, end: 7 }

Exemple bout-en-bout :

// app/api/odoo/partners/route.ts
import { withOdooAuth } from "odoo-nextjs/middleware";
import { OdooClient } from "odoo-nextjs";
import { toOdooPagination, fromOdooPagination } from "odoo-nextjs";

export const GET = withOdooAuth(async (req, { session }) => {
  const url = new URL(req.url);
  const { offset, limit } = toOdooPagination({
    page: Number(url.searchParams.get("page") ?? 1),
    pageSize: Number(url.searchParams.get("pageSize") ?? 20),
  });

  const [partners, total] = await Promise.all([
    OdooClient.searchRead(
      "res.partner",
      { domain: [["is_company", "=", true]], limit, offset, order: "name asc" },
      { sessionCookie: session.cookie },
    ),
    OdooClient.searchCount(
      "res.partner",
      { domain: [["is_company", "=", true]] },
      { sessionCookie: session.cookie },
    ),
  ]);

  return Response.json({
    partners,
    pagination: fromOdooPagination({ offset, limit, total }),
  });
});

Exemples

Trois projets Next.js prêts à l'emploi dans examples/ :

| Exemple | Démontre | |------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| | examples/basic-setup/ | login + liste de produits via withOdooAuth | | examples/portal-app/ | factures via withOdooPermissions + <PermissionsProvider> (utilisateurs portal) | | examples/full-erp-frontend/ | edge middleware, rate limiter, <ProtectedPage>, <RequireAccess>, pagination |

Pour lancer un exemple :

cd examples/full-erp-frontend
cp .env.example .env.local   # éditer ODOO_URL/DB/LOGIN/PASSWORD
npm install
npm run dev

Référence des erreurs

Toutes les erreurs héritent de OdooError et exposent code (HTTP-like) et data. Utilise les type guards exportés pour les distinguer :

import {
  OdooError,
  OdooSessionExpiredError,
  OdooAuthenticationError,
  OdooAccessError,
  OdooValidationError,
  OdooConnectionError,
  isSessionExpired,
  isAccessError,
  isOdooError,
} from "odoo-nextjs";

try {
  await OdooClient.searchRead(...);
} catch (err) {
  if (isSessionExpired(err)) {
    // → cookie expiré, redirige vers /login
  } else if (isAccessError(err)) {
    // → 403 Odoo (ACL ou record rule), afficher message d'erreur
  } else if (err instanceof OdooConnectionError) {
    // → Odoo injoignable
  } else if (isOdooError(err)) {
    // → autre erreur Odoo typée
  }
}

| Classe | Code | Cas | |------------------------------|-------|-------------------------------------------| | OdooSessionExpiredError | 100 | Session expirée | | OdooAuthenticationError | 401 | Identifiants invalides | | OdooAccessError | 403 | ACL / record rule refuse l'accès | | OdooValidationError | 400 | Données invalides côté Odoo | | OdooConnectionError | 503 | Odoo injoignable / timeout | | OdooError (base) | autre | Erreur générique |


Points d'entrée

| Import | Contenu | Environnement | | ----------------------------------- | ------------------------------------------------------ | ------------------ | | odoo-nextjs | core + auth + permissions + helpers | Node runtime | | odoo-nextjs/react | Provider, hooks, composants de garde | Client (Browser) | | odoo-nextjs/middleware | Wrappers API Routes, edge middleware, rate limiter | Node / Edge |

Règle de séparation stricte : src/react/ n'importe jamais de auth/, permissions/ ou middleware/. Les composants React communiquent avec Odoo uniquement via les API Routes Next.js (fetch vers /api/odoo/...).


Documentation

  • CLAUDE.md — spécification complète de l'API, types TypeScript, conventions, règles d'architecture.
  • CHANGELOG.md — historique des versions.
  • examples/ — projets Next.js prêts à l'emploi.

Licence

MIT