@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.
- package/dist/api/auth/token-checkout.d.ts +2 -2
- package/dist/api/auth/token-checkout.d.ts.map +1 -1
- package/dist/api/auth/token-checkout.js +21 -43
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/web/auth/TokenPage.d.ts.map +1 -1
- package/dist/web/auth/TokenPage.js +14 -7
- package/package.json +4 -4
- package/supabase/migrations/20251125000000_ai_tokens.sql +15 -13
- package/supabase/migrations/20251201000000_token_packs.sql +103 -36
|
@@ -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:
|
|
8
|
-
session_id:
|
|
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;
|
|
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,
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
package/dist/server.d.ts.map
CHANGED
|
@@ -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;
|
|
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":"
|
|
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
|
-
|
|
145
|
-
|
|
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-
|
|
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.
|
|
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.
|
|
39
|
-
"@lastbrain/core": "^2.0.
|
|
40
|
-
"@lastbrain/ui": "^2.0.
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
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;
|