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
Maintainers
Readme
odoo-nextjs
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
- Pourquoi ce package ?
- Installation
- Configuration initiale
- Guide d'utilisation
- 1. Protéger une API Route par session
- 2. Protéger une API Route par permissions Odoo
- 3. Lire / écrire des données via
OdooClient - 4. Construire des domaines Odoo typés
- 5. Authentifier un utilisateur côté serveur
- 6. Configurer le
OdooProvidercôté React - 7. Fetch + cache via
useOdooQuery - 8. Garde de rendu côté React
- 9. Middleware Edge (couche réseau)
- 10. Rate limiter
- 11. Pagination
- Exemples
- Référence des erreurs
- 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-nextjsnext (≥ 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_KEYdoit faire 32 octets minimum. Elle sert à chiffrer les mots de passe utilisateurs enMemorySessionStore. Sans elle, les sessions ne peuvent pas être renouvelées automatiquement parrenewFromStore.
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
sessionCookieest obligatoire pour les actions au nom d'un utilisateur. Sans lui,OdooClientutilise leserviceAccountconfiguré (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>
);
}⚠️
OdooProviderdoit être placé sousapp/layout.tsx. Les pages App Router qui consommentuseOdoo()ouusePermissions()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
withOdooAuthcô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 devRé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
