@lastbrain/module-ai 2.0.12 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ import { NextRequest, NextResponse } from "next/server";
4
4
  * Create a Stripe checkout session for token purchase
5
5
  */
6
6
  export declare function POST(request: NextRequest): Promise<NextResponse<{
7
- checkout_url: any;
8
- session_id: any;
7
+ checkout_url: string;
8
+ session_id: string;
9
9
  }> | NextResponse<{
10
10
  error: any;
11
11
  }>>;
@@ -1 +1 @@
1
- {"version":3,"file":"token-checkout.d.ts","sourceRoot":"","sources":["../../../src/api/auth/token-checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMxD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;IAgG9C"}
1
+ {"version":3,"file":"token-checkout.d.ts","sourceRoot":"","sources":["../../../src/api/auth/token-checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;IAiE9C"}
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
- import { getSupabaseServerClient, fetchInternalApi, } from "@lastbrain/core/server";
2
+ import { getSupabaseServerClient, getApiBaseUrl } from "@lastbrain/core/server";
3
+ import { createOneTimeCheckout } from "@lastbrain-labs/module-core-payment-pro/server";
3
4
  /**
4
5
  * POST /api/ai/auth/token-checkout
5
6
  * Create a Stripe checkout session for token purchase
@@ -27,49 +28,26 @@ export async function POST(request) {
27
28
  if (packError || !pack) {
28
29
  return NextResponse.json({ error: "Pack non trouvé" }, { status: 404 });
29
30
  }
30
- const baseUrl = process.env.APP_PRINCIPAL_DOMAIN ||
31
- process.env.NEXT_PUBLIC_APP_URL ||
32
- "http://localhost:3000";
33
- const successUrl = `${baseUrl}${"/cart/success"}`;
34
- const cancelUrl = `${baseUrl}${"/cart/cancel"}`;
35
- // Create checkout session via payment API
36
- const checkoutResponse = await fetchInternalApi("/api/auth/payment/checkout", {
37
- method: "POST",
38
- headers: {
39
- "Content-Type": "application/json",
40
- },
41
- body: JSON.stringify({
42
- items: [
43
- {
44
- name: pack.name,
45
- description: pack.description || `${pack.tokens} tokens`,
46
- price_cents: pack.price_cents,
47
- quantity: 1,
48
- },
49
- ],
50
- currency: pack.currency,
51
- mode: "one_time",
52
- success_url: successUrl,
53
- cancel_url: cancelUrl,
54
- metadata: {
55
- purpose: "token_purchase",
56
- module: "@lastbrain-labs/module-ai",
57
- token_pack_id: pack.id,
58
- tokens_amount: pack.tokens.toString(),
59
- user_id: user.id,
60
- },
61
- }),
62
- }, request);
63
- if (!checkoutResponse.ok) {
64
- const error = await checkoutResponse.json();
65
- throw new Error(error.error || "Erreur lors de la création du checkout");
66
- }
67
- const checkoutData = await checkoutResponse.json();
68
- const url = checkoutData.url || checkoutData.checkoutUrl || checkoutData.checkout_url;
69
- const sessionId = checkoutData.session_id || checkoutData.sessionId;
31
+ // Build absolute URLs with a fully-qualified base (works on Vercel, custom domains, local)
32
+ const baseUrl = getApiBaseUrl();
33
+ const successUrl = `${baseUrl.replace(/\/$/, "")}${"/cart/success"}`;
34
+ const cancelUrl = `${baseUrl.replace(/\/$/, "")}${"/cart/cancel"}`;
35
+ // Use central payment service directly instead of fetching internally
36
+ const checkoutResult = await createOneTimeCheckout({
37
+ purpose: "token",
38
+ module: "module-ai",
39
+ mode: "one_time",
40
+ owner_id: user.id,
41
+ user_id: user.id,
42
+ resourceType: "token_packs",
43
+ resourceId: pack.id,
44
+ successPath: "/cart/success",
45
+ cancelPath: "/cart/cancel",
46
+ description: `${pack.tokens} tokens IA`,
47
+ });
70
48
  return NextResponse.json({
71
- checkout_url: url,
72
- session_id: sessionId,
49
+ checkout_url: checkoutResult.url,
50
+ session_id: checkoutResult.sessionId,
73
51
  }, { status: 200 });
74
52
  }
75
53
  catch (error) {
package/dist/server.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface TokenLedgerEntry {
17
17
  amount: number;
18
18
  model?: string;
19
19
  prompt?: string;
20
- meta: Record<string, any>;
20
+ meta?: Record<string, any>;
21
21
  created_by?: string;
22
22
  created_at: string;
23
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,UAAU,GAAG,MAAM,GAAG,QAAiB,EAC7C,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EAC9B,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA+B/B;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAoD/B;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBrE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAW,EAClB,MAAM,GAAE,MAAU,GACjB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiB7B;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM;;;;;;GA4CjD;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,GAAE,MAAW,EAClB,MAAM,GAAE,MAAU;;;KAiBnB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,UAAU,GAAG,MAAM,GAAG,QAAiB,EAC7C,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EAC9B,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA+B/B;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAoD/B;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBrE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAW,EAClB,MAAM,GAAE,MAAU,GACjB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiB7B;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM;;;;;;GA4CjD;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,GAAE,MAAW,EAClB,MAAM,GAAE,MAAU;;;KAiBnB"}
package/dist/server.js CHANGED
@@ -124,7 +124,7 @@ export async function getTokenHistory(userId, limit = 50, offset = 0) {
124
124
  try {
125
125
  const { data, error } = await supabase
126
126
  .from("user_token_ledger")
127
- .select("*")
127
+ .select("amount,created_at,created_by,id,model,owner_id,prompt,ts,type")
128
128
  .eq("owner_id", userId)
129
129
  .order("ts", { ascending: false })
130
130
  .range(offset, offset + limit - 1);
@@ -1 +1 @@
1
- {"version":3,"file":"TokenPage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/TokenPage.tsx"],"names":[],"mappings":"AAgEA,wBAAgB,SAAS,4CAobxB"}
1
+ {"version":3,"file":"TokenPage.d.ts","sourceRoot":"","sources":["../../../src/web/auth/TokenPage.tsx"],"names":[],"mappings":"AAoEA,wBAAgB,SAAS,4CA4dxB"}
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useCallback } from "react";
4
4
  import { Card, CardBody, CardHeader, Spinner, Chip, Button, Select, SelectItem, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, addToast, } from "@lastbrain/ui";
5
- import { Coins, TrendingUp, TrendingDown, Calendar, ShoppingCart, AlertCircle, } from "lucide-react";
5
+ import { Coins, TrendingUp, TrendingDown, Calendar, ShoppingCart, AlertCircle, Gift, Eye, } from "lucide-react";
6
6
  import { useAuth } from "@lastbrain/core";
7
7
  export function TokenPage() {
8
8
  const { user } = useAuth();
@@ -30,13 +30,19 @@ export function TokenPage() {
30
30
  throw new Error("Erreur lors du chargement des données");
31
31
  }
32
32
  const data = await response.json();
33
+ // Historique complet pour calculs globaux
34
+ const allTransactions = data.history || [];
35
+ // Total offert/ajusté (tous les temps) via les transactions de type "adjust"
36
+ const totalGifted = allTransactions
37
+ .filter((t) => t.type === "adjust" && t.amount > 0)
38
+ .reduce((sum, t) => sum + t.amount, 0);
33
39
  setBalance({
34
40
  balance: data.balance || 0,
35
41
  totalAdded: data.stats?.totalPurchased + data.stats?.totalGifted || 0,
36
42
  totalUsed: data.stats?.totalUsed || 0,
43
+ totalGifted,
37
44
  });
38
45
  // Filtrer les transactions par mois
39
- const allTransactions = data.history || [];
40
46
  const filtered = allTransactions.filter((t) => t.created_at.startsWith(selectedMonth));
41
47
  // Calculer le running balance
42
48
  let runningBalance = data.balance || 0;
@@ -141,8 +147,9 @@ export function TokenPage() {
141
147
  }).format(cents / 100);
142
148
  };
143
149
  const formatTokensShort = (tokens) => {
144
- if (tokens >= 10000) {
145
- const thousands = Math.round(tokens / 1000);
150
+ // Affiche en K uniquement si c'est un millier rond, sinon la valeur exacte
151
+ if (tokens >= 1000 && tokens % 1000 === 0) {
152
+ const thousands = tokens / 1000;
146
153
  return `${thousands}K`;
147
154
  }
148
155
  return tokens.toLocaleString();
@@ -153,10 +160,10 @@ export function TokenPage() {
153
160
  if (loading) {
154
161
  return (_jsx("div", { className: "flex justify-center items-center min-h-96", children: _jsx(Spinner, { size: "lg" }) }));
155
162
  }
156
- return (_jsxs("div", { className: "container mx-auto p-2 md:p-6 max-w-7xl", children: [_jsxs("div", { className: "mb-6", children: [_jsx("h1", { className: "text-3xl font-bold mb-2", children: "Mes Tokens IA" }), _jsx("p", { className: "text-gray-500", children: "G\u00E9rez votre solde et consultez votre historique de consommation" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4 mb-6", children: [_jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsxs("div", { className: "flex items-center justify-center gap-2 mb-2", children: [_jsx(Coins, { size: 24, className: "text-primary" }), _jsx("h3", { className: "text-sm font-medium text-gray-500", children: "Solde actuel" })] }), _jsx("p", { className: "text-4xl font-bold text-primary", children: formatTokensShort(balance?.balance || 0) }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "tokens disponibles" })] }) }), _jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsxs("div", { className: "flex items-center justify-center gap-2 mb-2", children: [_jsx(TrendingUp, { size: 24, className: "text-success" }), _jsx("h3", { className: "text-sm font-medium text-gray-500", children: "Total" })] }), _jsx("p", { className: "text-4xl font-bold text-success", children: formatTokensShort(balance?.totalAdded || 0) }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "tokens achet\u00E9" })] }) }), _jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsxs("div", { className: "flex items-center justify-center gap-2 mb-2", children: [_jsx(TrendingDown, { size: 24, className: "text-danger" }), _jsx("h3", { className: "text-sm font-medium text-gray-500", children: "Total utilis\u00E9" })] }), _jsx("p", { className: "text-4xl font-bold text-danger", children: formatTokensShort(balance?.totalUsed || 0) }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "tokens consomm\u00E9s" })] }) })] }), monthlyStats && (_jsxs(Card, { className: "mb-6", children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between w-full", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { size: 20 }), _jsx("h3", { className: "text-lg font-semibold", children: "Statistiques du mois" })] }), _jsx(Select, { size: "sm", selectedKeys: [selectedMonth], onSelectionChange: (keys) => setSelectedMonth(Array.from(keys)[0]), className: "w-48", "aria-label": "S\u00E9lectionner un mois", children: availableMonths.map((month) => (_jsx(SelectItem, { textValue: month, children: new Date(month + "-01").toLocaleDateString("fr-FR", {
163
+ return (_jsxs("div", { className: " container mx-auto p-2 md:p-6 max-w-7xl", children: [_jsxs("div", { className: "mb-6", children: [_jsx("h1", { className: "text-3xl font-bold mb-2", children: "Mes Tokens IA" }), _jsx("p", { className: "text-gray-500", children: "G\u00E9rez votre solde et consultez votre historique de consommation" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-4 gap-4 mb-6", children: [_jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsxs("div", { className: "flex items-center justify-center gap-2 mb-2", children: [_jsx(Coins, { size: 24, className: "text-primary" }), _jsx("h3", { className: "text-sm font-medium text-gray-500", children: "Solde actuel" })] }), _jsx("p", { className: "text-4xl font-bold text-primary", children: formatTokensShort(balance?.balance || 0) }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "tokens disponibles" })] }) }), _jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsxs("div", { className: "flex items-center justify-center gap-2 mb-2", children: [_jsx(TrendingUp, { size: 24, className: "text-success" }), _jsx("h3", { className: "text-sm font-medium text-gray-500", children: "Total" })] }), _jsx("p", { className: "text-4xl font-bold text-success", children: formatTokensShort(balance?.totalAdded || 0) }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "tokens achet\u00E9" })] }) }), _jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsxs("div", { className: "flex items-center justify-center gap-2 mb-2", children: [_jsx(TrendingDown, { size: 24, className: "text-danger" }), _jsx("h3", { className: "text-sm font-medium text-gray-500", children: "Total utilis\u00E9" })] }), _jsx("p", { className: "text-4xl font-bold text-danger", children: formatTokensShort(balance?.totalUsed || 0) }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "tokens consomm\u00E9s" })] }) }), _jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsxs("div", { className: "flex items-center justify-center gap-2 mb-2", children: [_jsx(Gift, { size: 24, className: "text-warning" }), _jsx("h3", { className: "text-sm font-medium text-gray-500", children: "Tokens offerts / ajustements" })] }), _jsx("p", { className: "text-4xl font-bold text-warning", children: formatTokensShort(balance?.totalGifted || 0) }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "tokens cr\u00E9dit\u00E9s" })] }) })] }), monthlyStats && (_jsxs(Card, { className: "mb-6", children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between w-full", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { size: 20 }), _jsx("h3", { className: "text-lg font-semibold", children: "Statistiques du mois" })] }), _jsx(Select, { size: "sm", selectedKeys: [selectedMonth], onSelectionChange: (keys) => setSelectedMonth(Array.from(keys)[0]), className: "w-48", "aria-label": "S\u00E9lectionner un mois", children: availableMonths.map((month) => (_jsx(SelectItem, { textValue: month, children: new Date(month + "-01").toLocaleDateString("fr-FR", {
157
164
  month: "long",
158
165
  year: "numeric",
159
- }) }, month))) })] }) }), _jsx(CardBody, { children: _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "text-center p-4 bg-success-50 dark:bg-success-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Ajout\u00E9s" }), _jsxs("p", { className: "text-2xl font-bold text-success", children: ["+", formatTokensShort(monthlyStats.added)] })] }), _jsxs("div", { className: "text-center p-4 bg-danger-50 dark:bg-danger-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Utilis\u00E9s" }), _jsxs("p", { className: "text-2xl font-bold text-danger", children: ["-", formatTokensShort(monthlyStats.used)] })] }), _jsxs("div", { className: "text-center p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Solde net" }), _jsxs("p", { className: `text-2xl font-bold ${monthlyStats.net >= 0 ? "text-success" : "text-danger"}`, children: [monthlyStats.net >= 0 ? "+" : "", formatTokensShort(Math.abs(monthlyStats.net))] })] })] }) })] })), _jsxs(Card, { className: "mb-6", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Historique des transactions" }) }), _jsx(CardBody, { children: transactions.length === 0 ? (_jsxs("div", { className: "text-center py-12 text-gray-500", children: [_jsx(Coins, { size: 48, className: "mx-auto mb-4 opacity-20" }), _jsx("p", { children: "Aucune transaction pour ce mois" })] })) : (_jsxs(Table, { "aria-label": "Historique des transactions", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "DATE" }), _jsx(TableColumn, { children: "TYPE" }), _jsx(TableColumn, { children: "DESCRIPTION" }), _jsx(TableColumn, { children: "MONTANT" }), _jsx(TableColumn, { children: "SOLDE APR\u00C8S" })] }), _jsx(TableBody, { children: transactions.map((transaction) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx("span", { className: "text-sm text-gray-600", children: formatDate(transaction.created_at) }) }), _jsx(TableCell, { children: _jsx(Chip, { size: "sm", variant: "flat", color: getTypeColor(transaction.type), children: getTypeLabel(transaction.type) }) }), _jsx(TableCell, { children: _jsx("div", { className: "max-w-md", children: _jsx("p", { className: "text-sm truncate", children: transaction.description || transaction.model || "-" }) }) }), _jsx(TableCell, { children: _jsxs("span", { className: `font-semibold ${transaction.amount > 0
166
+ }) }, month))) })] }) }), _jsx(CardBody, { children: _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "text-center p-4 bg-success-50 dark:bg-success-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Ajout\u00E9s" }), _jsxs("p", { className: "text-2xl font-bold text-success", children: ["+", formatTokensShort(monthlyStats.added)] })] }), _jsxs("div", { className: "text-center p-4 bg-danger-50 dark:bg-danger-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Utilis\u00E9s" }), _jsxs("p", { className: "text-2xl font-bold text-danger", children: ["-", formatTokensShort(monthlyStats.used)] })] }), _jsxs("div", { className: "text-center p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Solde net" }), _jsxs("p", { className: `text-2xl font-bold ${monthlyStats.net >= 0 ? "text-success" : "text-danger"}`, children: [monthlyStats.net >= 0 ? "+" : "", formatTokensShort(Math.abs(monthlyStats.net))] })] })] }) })] })), _jsxs(Card, { className: "mb-6", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Historique des transactions" }) }), _jsx(CardBody, { children: transactions.length === 0 ? (_jsxs("div", { className: "text-center py-12 text-gray-500", children: [_jsx(Coins, { size: 48, className: "mx-auto mb-4 opacity-20" }), _jsx("p", { children: "Aucune transaction pour ce mois" })] })) : (_jsxs(Table, { "aria-label": "Historique des transactions", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { className: "min-w-[120px]", children: "DATE" }), _jsx(TableColumn, { children: "TYPE" }), _jsx(TableColumn, { children: "DESCRIPTION" }), _jsx(TableColumn, { children: "MONTANT" }), _jsx(TableColumn, { children: "PROMPT" }), _jsx(TableColumn, { align: "end", children: "SOLDE APR\u00C8S" })] }), _jsx(TableBody, { children: transactions.map((transaction) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx("span", { className: " text-sm text-gray-600", children: formatDate(transaction.created_at) }) }), _jsx(TableCell, { children: _jsx(Chip, { size: "sm", variant: "flat", color: getTypeColor(transaction.type), children: getTypeLabel(transaction.type) }) }), _jsx(TableCell, { children: _jsx("div", { className: "max-w-md", children: _jsx("p", { className: "text-sm truncate", children: transaction.description || transaction.model || "-" }) }) }), _jsx(TableCell, { children: _jsxs("span", { className: `font-semibold ${transaction.amount > 0
160
167
  ? "text-success"
161
- : "text-danger"}`, children: [transaction.amount > 0 ? "+" : "", formatTokensShort(Math.abs(transaction.amount))] }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-sm text-gray-600", children: formatTokensShort(transaction.running_balance) }) })] }, transaction.id))) })] })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ShoppingCart, { size: 20 }), _jsx("h3", { className: "text-lg font-semibold", children: "Acheter des tokens" })] }) }), _jsx(CardBody, { children: tokenPacks.length === 0 ? (_jsxs("div", { className: "text-center py-8", children: [_jsx(ShoppingCart, { size: 48, className: "mx-auto mb-4 text-gray-300 dark:text-gray-600" }), _jsx("p", { className: "text-gray-500 mb-4", children: "Aucun pack disponible pour le moment" })] })) : (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4", children: tokenPacks.map((pack) => (_jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsx("div", { className: "mx-auto", children: _jsx(Coins, { size: 24, className: "mx-auto mb-2 text-gray-400" }) }), _jsx("h4", { className: "text-lg font-bold mb-2", children: pack.name }), pack.description && (_jsx("p", { className: "text-sm text-gray-500 mb-4", children: pack.description })), _jsxs("div", { className: "mb-4", children: [_jsx("p", { className: "text-3xl font-bold text-primary", children: formatTokensShort(pack.tokens) }), _jsx("p", { className: "text-xs text-gray-400", children: "tokens" })] }), _jsx("p", { className: "text-xl font-semibold mb-4", children: formatPrice(pack.price_cents, pack.currency) }), _jsx(Button, { color: "primary", className: "w-full", onPress: () => handleBuyTokens(pack.id), isLoading: checkoutLoading === pack.id, startContent: checkoutLoading !== pack.id && (_jsx(ShoppingCart, { size: 16 })), children: "Acheter" })] }) }, pack.id))) })) })] })] }));
168
+ : "text-danger"}`, children: [transaction.amount > 0 ? "+" : "", formatTokensShort(Math.abs(transaction.amount))] }) }), _jsx(TableCell, { children: transaction.prompt && (_jsxs("div", { className: "flex flex-inline gap-2 items-center", children: [_jsx("p", { className: "max-w-xs truncate", children: transaction.prompt?.slice(0, 100) }), _jsx(Button, { size: "sm", variant: "flat", isIconOnly: true, className: "truncate max-w-xs", startContent: _jsx(Eye, { size: 12 }) })] })) }), _jsx(TableCell, { children: _jsx("span", { className: "text-sm text-gray-600", children: formatTokensShort(transaction.running_balance) }) })] }, transaction.id))) })] })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ShoppingCart, { size: 20 }), _jsx("h3", { className: "text-lg font-semibold", children: "Acheter des tokens" })] }) }), _jsx(CardBody, { children: tokenPacks.length === 0 ? (_jsxs("div", { className: "text-center py-8", children: [_jsx(ShoppingCart, { size: 48, className: "mx-auto mb-4 text-gray-300 dark:text-gray-600" }), _jsx("p", { className: "text-gray-500 mb-4", children: "Aucun pack disponible pour le moment" })] })) : (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4", children: tokenPacks.map((pack) => (_jsx(Card, { children: _jsxs(CardBody, { className: "text-center py-6", children: [_jsx("div", { className: "mx-auto", children: _jsx(Coins, { size: 24, className: "mx-auto mb-2 text-gray-400" }) }), _jsx("h4", { className: "text-lg font-bold mb-2", children: pack.name }), pack.description && (_jsx("p", { className: "text-sm text-gray-500 mb-4", children: pack.description })), _jsxs("div", { className: "mb-4", children: [_jsx("p", { className: "text-3xl font-bold text-primary", children: formatTokensShort(pack.tokens) }), _jsx("p", { className: "text-xs text-gray-400", children: "tokens" })] }), _jsx("p", { className: "text-xl font-semibold mb-4", children: formatPrice(pack.price_cents, pack.currency) }), _jsx(Button, { color: "primary", className: "w-full", onPress: () => handleBuyTokens(pack.id), isLoading: checkoutLoading === pack.id, startContent: checkoutLoading !== pack.id && (_jsx(ShoppingCart, { size: 16 })), children: "Acheter" })] }) }, pack.id))) })) })] })] }));
162
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/module-ai",
3
- "version": "2.0.12",
3
+ "version": "2.0.15",
4
4
  "description": "Module de génération IA (texte et images) avec gestion de tokens pour LastBrain",
5
5
  "private": false,
6
6
  "release": {
@@ -35,9 +35,9 @@
35
35
  "@heroui/react": "^2.4.23",
36
36
  "@heroui/system": "^2.4.23",
37
37
  "@heroui/theme": "^2.4.23",
38
- "@lastbrain-labs/module-core-payment-pro": "^2.0.12",
39
- "@lastbrain/core": "^2.0.13",
40
- "@lastbrain/ui": "^2.0.13",
38
+ "@lastbrain-labs/module-core-payment-pro": "^2.0.15",
39
+ "@lastbrain/core": "^2.0.16",
40
+ "@lastbrain/ui": "^2.0.16",
41
41
  "lucide-react": "^0.554.0",
42
42
  "next": "^16.0.7",
43
43
  "openai": "^6.9.1",
@@ -35,7 +35,7 @@ CREATE INDEX IF NOT EXISTS idx_user_token_ledger_usages ON public.user_token_led
35
35
  -- Solde courant par utilisateur
36
36
 
37
37
  DROP VIEW IF EXISTS public.user_token_balance_v;
38
- CREATE VIEW public.user_token_balance_v AS
38
+ CREATE VIEW public.user_token_balance_v WITH (security_invoker=true) AS
39
39
  SELECT
40
40
  owner_id,
41
41
  COALESCE(SUM(amount), 0)::BIGINT AS balance
@@ -71,7 +71,8 @@ BEGIN
71
71
 
72
72
  RETURN NEW;
73
73
  END;
74
- $$ LANGUAGE plpgsql;
74
+ $$ LANGUAGE plpgsql
75
+ SET search_path = public;
75
76
 
76
77
  DROP TRIGGER IF EXISTS trigger_check_token_balance ON public.user_token_ledger;
77
78
  CREATE TRIGGER trigger_check_token_balance
@@ -91,8 +92,8 @@ CREATE POLICY user_token_ledger_select_own
91
92
  ON public.user_token_ledger
92
93
  FOR SELECT
93
94
  USING (
94
- owner_id = auth.uid()
95
- OR is_superadmin(auth.uid())
95
+ owner_id = (SELECT auth.uid())
96
+ OR is_superadmin((SELECT auth.uid()))
96
97
  );
97
98
 
98
99
  -- Policy: Les utilisateurs peuvent créer leurs propres entrées de type 'use'
@@ -101,8 +102,8 @@ CREATE POLICY user_token_ledger_insert_own
101
102
  ON public.user_token_ledger
102
103
  FOR INSERT
103
104
  WITH CHECK (
104
- (owner_id = auth.uid() AND type = 'use')
105
- OR is_superadmin(auth.uid())
105
+ (owner_id = (SELECT auth.uid()) AND type = 'use')
106
+ OR is_superadmin((SELECT auth.uid()))
106
107
  );
107
108
 
108
109
  -- Policy: Seuls les superadmins peuvent update
@@ -110,14 +111,14 @@ DROP POLICY IF EXISTS user_token_ledger_update_admin ON public.user_token_ledger
110
111
  CREATE POLICY user_token_ledger_update_admin
111
112
  ON public.user_token_ledger
112
113
  FOR UPDATE
113
- USING (is_superadmin(auth.uid()));
114
+ USING (is_superadmin((SELECT auth.uid())));
114
115
 
115
116
  -- Policy: Seuls les superadmins peuvent delete
116
117
  DROP POLICY IF EXISTS user_token_ledger_delete_admin ON public.user_token_ledger;
117
118
  CREATE POLICY user_token_ledger_delete_admin
118
119
  ON public.user_token_ledger
119
120
  FOR DELETE
120
- USING (is_superadmin(auth.uid()));
121
+ USING (is_superadmin((SELECT auth.uid())));
121
122
 
122
123
  -- =====================================================
123
124
  -- Table: user_prompts
@@ -150,28 +151,28 @@ DROP POLICY IF EXISTS "Users can view own prompts" ON public.user_prompts;
150
151
  CREATE POLICY "Users can view own prompts"
151
152
  ON public.user_prompts
152
153
  FOR SELECT
153
- USING (auth.uid() = owner_id);
154
+ USING ((SELECT auth.uid()) = owner_id);
154
155
 
155
156
  -- Politique: Les utilisateurs peuvent insérer leurs propres prompts
156
157
  DROP POLICY IF EXISTS "Users can insert own prompts" ON public.user_prompts;
157
158
  CREATE POLICY "Users can insert own prompts"
158
159
  ON public.user_prompts
159
160
  FOR INSERT
160
- WITH CHECK (auth.uid() = owner_id);
161
+ WITH CHECK ((SELECT auth.uid()) = owner_id);
161
162
 
162
163
  -- Politique: Les utilisateurs peuvent modifier leurs propres prompts
163
164
  DROP POLICY IF EXISTS "Users can update own prompts" ON public.user_prompts;
164
165
  CREATE POLICY "Users can update own prompts"
165
166
  ON public.user_prompts
166
167
  FOR UPDATE
167
- USING (auth.uid() = owner_id);
168
+ USING ((SELECT auth.uid()) = owner_id);
168
169
 
169
170
  -- Politique: Les utilisateurs peuvent supprimer leurs propres prompts
170
171
  DROP POLICY IF EXISTS "Users can delete own prompts" ON public.user_prompts;
171
172
  CREATE POLICY "Users can delete own prompts"
172
173
  ON public.user_prompts
173
174
  FOR DELETE
174
- USING (auth.uid() = owner_id);
175
+ USING ((SELECT auth.uid()) = owner_id);
175
176
 
176
177
  -- Trigger pour updated_at
177
178
  CREATE OR REPLACE FUNCTION public.update_user_prompts_updated_at()
@@ -180,7 +181,8 @@ BEGIN
180
181
  NEW.updated_at = NOW();
181
182
  RETURN NEW;
182
183
  END;
183
- $$ LANGUAGE plpgsql;
184
+ $$ LANGUAGE plpgsql
185
+ SET search_path = public;
184
186
 
185
187
  DROP TRIGGER IF EXISTS update_user_prompts_updated_at ON public.user_prompts;
186
188
  CREATE TRIGGER update_user_prompts_updated_at
@@ -3,42 +3,100 @@
3
3
  -- Migration: 20251201000000_token_packs.sql
4
4
  -- Description: Token packs for purchasing tokens via Stripe
5
5
  -- ===========================================================================
6
-
7
6
  -- ===========================================================================
8
7
  -- Table: public.token_packs
9
8
  -- Defines available token packs that users can purchase
10
9
  -- ===========================================================================
11
- CREATE TABLE IF NOT EXISTS public.token_packs (
12
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
13
- name text NOT NULL, -- Display name (e.g., "100 Tokens")
14
- description text NULL, -- Optional description
15
- tokens integer NOT NULL, -- Number of tokens in pack
16
- price_cents integer NOT NULL, -- Price in cents
17
- currency text NOT NULL DEFAULT 'EUR', -- ISO currency code
18
- stripe_price_id text NULL, -- Optional Stripe price ID for recurring
19
- is_active boolean NOT NULL DEFAULT true, -- Whether pack is available for purchase
20
- sort_order integer NOT NULL DEFAULT 0, -- Display order
21
- created_at timestamptz NOT NULL DEFAULT now(),
22
- updated_at timestamptz NOT NULL DEFAULT now()
23
- );
10
+ CREATE TABLE
11
+ IF NOT EXISTS public.token_packs (
12
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid (),
13
+ name text NOT NULL, -- Display name (e.g., "100 Tokens")
14
+ description text NULL, -- Optional description
15
+ tokens integer NOT NULL, -- Number of tokens in pack
16
+ price_cents integer NOT NULL, -- Price in cents
17
+ currency text NOT NULL DEFAULT 'EUR', -- ISO currency code
18
+ stripe_price_id text NULL, -- Optional Stripe price ID for recurring
19
+ is_active boolean NOT NULL DEFAULT true, -- Whether pack is available for purchase
20
+ sort_order integer NOT NULL DEFAULT 0, -- Display order
21
+ created_at timestamptz NOT NULL DEFAULT now (),
22
+ updated_at timestamptz NOT NULL DEFAULT now ()
23
+ );
24
24
 
25
25
  -- ===========================================================================
26
26
  -- Indexes
27
27
  -- ===========================================================================
28
- CREATE INDEX IF NOT EXISTS idx_token_packs_active_order
29
- ON public.token_packs(is_active, sort_order);
28
+ CREATE INDEX IF NOT EXISTS idx_token_packs_active_order ON public.token_packs (is_active, sort_order);
30
29
 
31
30
  -- ===========================================================================
32
31
  -- Insert default token packs
33
32
  -- ===========================================================================
34
- INSERT INTO public.token_packs (name, description, tokens, price_cents, currency, sort_order)
35
- VALUES
36
- ('Starter', 'Starter pack', 100000, 500, 'EUR', 1),
37
- ('Standard', 'Standard pack', 250000, 1000, 'EUR', 2),
38
- ('Premium', 'Premium pack', 600000, 2000, 'EUR', 3),
39
- ('Creator', 'Creator pack', 2000000, 5000, 'EUR', 4)
40
-
41
- ON CONFLICT DO NOTHING;
33
+ INSERT INTO
34
+ "public"."token_packs" (
35
+ "id",
36
+ "name",
37
+ "description",
38
+ "tokens",
39
+ "price_cents",
40
+ "currency",
41
+ "stripe_price_id",
42
+ "is_active",
43
+ "sort_order",
44
+ "created_at",
45
+ "updated_at"
46
+ )
47
+ VALUES
48
+ (
49
+ '6143c146-e938-4015-90fe-bc24af5df375',
50
+ 'Starter',
51
+ 'Starter pack',
52
+ '100000',
53
+ '500',
54
+ 'EUR',
55
+ null,
56
+ 'true',
57
+ '1',
58
+ '2025-12-01 14:21:15.897966+00',
59
+ '2025-12-01 14:21:15.897966+00'
60
+ ),
61
+ (
62
+ 'b46fb61b-1ce2-4ca0-b001-5371797d8bc2',
63
+ 'Premium',
64
+ 'Premium pack',
65
+ '600000',
66
+ '2000',
67
+ 'EUR',
68
+ null,
69
+ 'true',
70
+ '3',
71
+ '2025-12-01 14:21:15.897966+00',
72
+ '2025-12-01 14:21:15.897966+00'
73
+ ),
74
+ (
75
+ 'beaf2011-097d-4123-aaf6-d7163cf3a8d0',
76
+ 'Standard',
77
+ 'Standard pack',
78
+ '250000',
79
+ '1000',
80
+ 'EUR',
81
+ null,
82
+ 'true',
83
+ '2',
84
+ '2025-12-01 14:21:15.897966+00',
85
+ '2025-12-01 14:21:15.897966+00'
86
+ ),
87
+ (
88
+ 'd9a438fe-614c-4054-ac3e-3bedff4cd5a0',
89
+ 'Creator',
90
+ 'Creator pack',
91
+ '2000000',
92
+ '5000',
93
+ 'EUR',
94
+ null,
95
+ 'true',
96
+ '4',
97
+ '2025-12-01 14:21:15.897966+00',
98
+ '2025-12-01 14:21:15.897966+00'
99
+ ) ON CONFLICT DO NOTHING;
42
100
 
43
101
  -- ===========================================================================
44
102
  -- RLS (Row Level Security)
@@ -47,27 +105,36 @@ ALTER TABLE public.token_packs ENABLE ROW LEVEL SECURITY;
47
105
 
48
106
  -- Policy: Everyone can view active packs
49
107
  DROP POLICY IF EXISTS token_packs_select_public ON public.token_packs;
50
- CREATE POLICY token_packs_select_public ON public.token_packs
51
- FOR SELECT
52
- USING (is_active = true OR is_superadmin(auth.uid()));
108
+
109
+ CREATE POLICY token_packs_select_public ON public.token_packs FOR
110
+ SELECT
111
+ USING (
112
+ is_active = true
113
+ OR is_superadmin (auth.uid ())
114
+ );
53
115
 
54
116
  -- Policy: Only superadmins can insert/update/delete
55
117
  DROP POLICY IF EXISTS token_packs_admin_all ON public.token_packs;
56
- CREATE POLICY token_packs_admin_all ON public.token_packs
57
- FOR ALL
58
- USING (is_superadmin(auth.uid()));
118
+
119
+ CREATE POLICY token_packs_admin_all ON public.token_packs FOR ALL USING (is_superadmin (auth.uid ()));
59
120
 
60
121
  -- ===========================================================================
61
122
  -- Trigger for updated_at
62
123
  -- ===========================================================================
63
124
  DROP TRIGGER IF EXISTS set_token_packs_updated_at ON public.token_packs;
64
- CREATE TRIGGER set_token_packs_updated_at
65
- BEFORE UPDATE ON public.token_packs
66
- FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
125
+
126
+ CREATE TRIGGER set_token_packs_updated_at BEFORE
127
+ UPDATE ON public.token_packs FOR EACH ROW EXECUTE FUNCTION public.set_updated_at ();
67
128
 
68
129
  -- ===========================================================================
69
130
  -- Grants
70
131
  -- ===========================================================================
71
- GRANT SELECT ON public.token_packs TO anon;
72
- GRANT SELECT ON public.token_packs TO authenticated;
73
- GRANT ALL ON public.token_packs TO service_role;
132
+ GRANT
133
+ SELECT
134
+ ON public.token_packs TO anon;
135
+
136
+ GRANT
137
+ SELECT
138
+ ON public.token_packs TO authenticated;
139
+
140
+ GRANT ALL ON public.token_packs TO service_role;