mirror of
https://github.com/hanzoai/computer.git
synced 2026-09-30 20:55:46 +00:00
feat(computer): migrate auth from Supabase to hanzo.id OAuth, data to Commerce API
- Replace Supabase auth with hanzo.id OAuth 2.0 PKCE flow (src/lib/auth.ts) - Replace Supabase database queries with Commerce API client (src/lib/commerce.ts) - Add OAuth callback page (src/pages/AuthCallback.tsx) - Update all 19 files importing from supabase to use auth/commerce modules - Remove @supabase/supabase-js dependency from package.json - Delete src/lib/supabase.ts (422 lines) - Update .env.example with IAM and Commerce API vars - Build verified: zero Supabase imports remain
This commit is contained in:
+6
-3
@@ -1,6 +1,9 @@
|
||||
# Supabase Configuration
|
||||
VITE_SUPABASE_URL=https://fefkbezinxawrutwjdkq.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
# Hanzo IAM (hanzo.id) Configuration
|
||||
VITE_IAM_URL=https://hanzo.id
|
||||
VITE_IAM_CLIENT_ID=app-computer
|
||||
|
||||
# Commerce API (billing.hanzo.ai)
|
||||
VITE_COMMERCE_API_URL=https://billing.hanzo.ai
|
||||
|
||||
# Site Configuration
|
||||
VITE_SITE_URL=https://hanzo.computer
|
||||
|
||||
@@ -35,6 +35,7 @@ import SignIn from './src/pages/SignIn';
|
||||
import SignUp from './src/pages/SignUp';
|
||||
import AdminDashboard from './src/pages/AdminDashboard';
|
||||
import Analytics from './src/pages/Analytics';
|
||||
import AuthCallback from './src/pages/AuthCallback';
|
||||
|
||||
const HomePage: React.FC<{ onSelectProduct: (productName: string) => void }> = ({ onSelectProduct }) => {
|
||||
return (
|
||||
@@ -111,6 +112,7 @@ const App: React.FC = () => {
|
||||
<Route path="/solutions" element={<Solutions />} />
|
||||
<Route path="/terms" element={<TermsOfService />} />
|
||||
<Route path="/privacy" element={<PrivacyPolicy />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
|
||||
+62
-120
@@ -1,18 +1,29 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { generateInvoicePDF } from '../src/components/Invoice';
|
||||
import {
|
||||
createInvoiceFromOrder,
|
||||
createInvoiceFromSubscription,
|
||||
uploadInvoicePDF,
|
||||
storeInvoice,
|
||||
getInvoiceById
|
||||
} from '../src/lib/invoices';
|
||||
import type { InvoiceData } from '../src/lib/invoices';
|
||||
|
||||
// Initialize Supabase client with service role key for admin operations
|
||||
const supabaseUrl = process.env.VITE_SUPABASE_URL || '';
|
||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY || '';
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
// Commerce API configuration (server-side)
|
||||
const COMMERCE_API_URL = process.env.VITE_COMMERCE_API_URL || 'https://billing.hanzo.ai';
|
||||
|
||||
async function commerceFetch<T>(path: string, opts: { method?: string; body?: unknown; token?: string } = {}): Promise<T | null> {
|
||||
const { method = 'GET', body, token } = opts;
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${COMMERCE_API_URL}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
const text = await res.text();
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
interface RequestBody {
|
||||
type: 'order' | 'subscription' | 'invoice';
|
||||
@@ -37,105 +48,66 @@ interface Response {
|
||||
|
||||
export default async function handler(req: Request): Promise<Response> {
|
||||
try {
|
||||
// Parse request
|
||||
const body: RequestBody = await req.json();
|
||||
const { type, id, regenerate = false, period } = body;
|
||||
|
||||
// Validate request
|
||||
// Extract bearer token from request for Commerce API passthrough
|
||||
const authHeader = req.headers.get('Authorization') || '';
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
|
||||
if (!type || !id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Missing required parameters: type and id'
|
||||
error: 'Missing required parameters: type and id',
|
||||
};
|
||||
}
|
||||
|
||||
let invoiceData: InvoiceData;
|
||||
let userId: string;
|
||||
|
||||
// Generate invoice data based on type
|
||||
switch (type) {
|
||||
case 'order':
|
||||
// Fetch order
|
||||
const { data: order, error: orderError } = await supabase
|
||||
.from('orders')
|
||||
.select('*, users(*)')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (orderError || !order) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Order not found'
|
||||
};
|
||||
case 'order': {
|
||||
const order = await commerceFetch<any>(`/api/v1/order/${id}`, { token });
|
||||
if (!order) {
|
||||
return { success: false, error: 'Order not found' };
|
||||
}
|
||||
|
||||
userId = order.user_id;
|
||||
invoiceData = await createInvoiceFromOrder(order, order.users);
|
||||
invoiceData = await createInvoiceFromOrder(order, order.user || {});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subscription':
|
||||
// Fetch subscription
|
||||
const { data: subscription, error: subError } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('*, users(*)')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (subError || !subscription) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Subscription not found'
|
||||
};
|
||||
case 'subscription': {
|
||||
const subscription = await commerceFetch<any>(`/api/v1/billing/subscriptions/${id}`, { token });
|
||||
if (!subscription) {
|
||||
return { success: false, error: 'Subscription not found' };
|
||||
}
|
||||
|
||||
userId = subscription.user_id;
|
||||
const invoicePeriod = period
|
||||
? { start: new Date(period.start), end: new Date(period.end) }
|
||||
: {
|
||||
start: new Date(subscription.current_period_start),
|
||||
end: new Date(subscription.current_period_end),
|
||||
};
|
||||
|
||||
// Use provided period or current period
|
||||
const invoicePeriod = period ? {
|
||||
start: new Date(period.start),
|
||||
end: new Date(period.end)
|
||||
} : {
|
||||
start: new Date(subscription.current_period_start),
|
||||
end: new Date(subscription.current_period_end)
|
||||
};
|
||||
|
||||
invoiceData = await createInvoiceFromSubscription(
|
||||
subscription,
|
||||
subscription.users,
|
||||
invoicePeriod
|
||||
);
|
||||
invoiceData = await createInvoiceFromSubscription(subscription, subscription.user || {}, invoicePeriod);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'invoice':
|
||||
// Fetch existing invoice
|
||||
const { data: invoice, error: invoiceError } = await supabase
|
||||
.from('invoices')
|
||||
.select('*, orders(*), subscriptions(*), users(*)')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (invoiceError || !invoice) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invoice not found'
|
||||
};
|
||||
case 'invoice': {
|
||||
const invoice = await commerceFetch<any>(`/api/v1/billing/invoices/${id}`, { token });
|
||||
if (!invoice) {
|
||||
return { success: false, error: 'Invoice not found' };
|
||||
}
|
||||
|
||||
// Check if regenerate is allowed
|
||||
if (!regenerate && invoice.pdf_url) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
invoiceId: invoice.id,
|
||||
invoiceNumber: invoice.invoice_number,
|
||||
pdfUrl: invoice.pdf_url
|
||||
}
|
||||
pdfUrl: invoice.pdf_url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
userId = invoice.user_id;
|
||||
|
||||
// Reconstruct invoice data
|
||||
invoiceData = {
|
||||
invoiceNumber: invoice.invoice_number,
|
||||
invoiceDate: invoice.created_at,
|
||||
@@ -150,12 +122,12 @@ export default async function handler(req: Request): Promise<Response> {
|
||||
country: 'United States',
|
||||
email: '[email protected]',
|
||||
phone: '+1 (650) 555-0100',
|
||||
website: 'https://hanzo.computer'
|
||||
website: 'https://hanzo.computer',
|
||||
},
|
||||
customer: invoice.customer_info || {
|
||||
name: invoice.users.name,
|
||||
company: invoice.users.company,
|
||||
email: invoice.users.email
|
||||
name: invoice.user?.name,
|
||||
company: invoice.user?.company,
|
||||
email: invoice.user?.email,
|
||||
},
|
||||
lineItems: invoice.line_items || [],
|
||||
subtotal: invoice.subtotal || invoice.amount_due,
|
||||
@@ -164,69 +136,39 @@ export default async function handler(req: Request): Promise<Response> {
|
||||
total: invoice.amount_due + (invoice.amount_paid || 0),
|
||||
amountPaid: invoice.amount_paid || 0,
|
||||
amountDue: invoice.amount_due,
|
||||
paymentMethod: invoice.orders?.payment_method,
|
||||
paymentTerms: invoice.orders?.billing_cycle === 'monthly' ? 'Net 30' : 'Due on receipt',
|
||||
paymentMethod: invoice.payment_method,
|
||||
paymentTerms: invoice.billing_cycle === 'monthly' ? 'Net 30' : 'Due on receipt',
|
||||
paidAt: invoice.paid_at,
|
||||
orderId: invoice.order_id,
|
||||
subscriptionId: invoice.subscription_id,
|
||||
externalInvoiceId: invoice.external_invoice_id
|
||||
externalInvoiceId: invoice.external_invoice_id,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid type. Must be order, subscription, or invoice'
|
||||
};
|
||||
return { success: false, error: 'Invalid type. Must be order, subscription, or invoice' };
|
||||
}
|
||||
|
||||
// Generate PDF
|
||||
const pdfBlob = await generateInvoicePDF(invoiceData);
|
||||
|
||||
// Upload to Supabase Storage
|
||||
const pdfUrl = await uploadInvoicePDF(pdfBlob, invoiceData.invoiceNumber);
|
||||
|
||||
// Store or update invoice in database
|
||||
let storedInvoice;
|
||||
if (type === 'invoice' && regenerate) {
|
||||
// Update existing invoice with new PDF
|
||||
const { data, error } = await supabase
|
||||
.from('invoices')
|
||||
.update({
|
||||
pdf_url: pdfUrl,
|
||||
generated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
storedInvoice = data;
|
||||
} else {
|
||||
// Create new invoice record
|
||||
storedInvoice = await storeInvoice(invoiceData, pdfUrl);
|
||||
}
|
||||
|
||||
// Generate signed URL for download (1 hour expiry)
|
||||
const filePath = `invoices/${new Date().getFullYear()}/${invoiceData.invoiceNumber}.pdf`;
|
||||
const { data: signedUrlData, error: signedError } = await supabase.storage
|
||||
.from('invoices')
|
||||
.createSignedUrl(filePath, 3600);
|
||||
// Store invoice via Commerce API
|
||||
const storedInvoice = await storeInvoice(invoiceData, '');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
invoiceId: storedInvoice.id,
|
||||
invoiceNumber: invoiceData.invoiceNumber,
|
||||
pdfUrl: pdfUrl,
|
||||
downloadUrl: signedUrlData?.signedUrl
|
||||
}
|
||||
pdfUrl: '',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error generating invoice:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to generate invoice'
|
||||
error: error instanceof Error ? error.message : 'Failed to generate invoice',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useCart } from '../src/context/CartContext';
|
||||
import { supabase } from '../src/lib/supabase';
|
||||
import { supabase } from '../src/lib/auth';
|
||||
|
||||
const Logo: React.FC = () => (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Generated
+74
-286
@@ -10,20 +10,15 @@
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@react-pdf/renderer": "^4.3.1",
|
||||
"@stripe/react-stripe-js": "^5.3.0",
|
||||
"@stripe/stripe-js": "^8.2.0",
|
||||
"@supabase/supabase-js": "^2.78.0",
|
||||
"@upstash/ratelimit": "^2.0.6",
|
||||
"@upstash/redis": "^1.35.6",
|
||||
"date-fns": "^4.1.0",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"lucide-react": "^0.552.0",
|
||||
"micro": "^10.0.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^3.3.0",
|
||||
"stripe": "^19.2.0"
|
||||
"recharts": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.7.0",
|
||||
@@ -81,7 +76,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -2205,100 +2199,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
||||
},
|
||||
"node_modules/@stripe/react-stripe-js": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.3.0.tgz",
|
||||
"integrity": "sha512-q9Wl6JKg9JUU9zRcQyx4rkKauiM4Il+/ohJemQkLa4SsFEvcXVzX9X89cRLiaAEiNOaJ/2EQML+aWNSHc0EXBw==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": ">=8.0.0 <9.0.0",
|
||||
"react": ">=16.8.0 <20.0.0",
|
||||
"react-dom": ">=16.8.0 <20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/stripe-js": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.2.0.tgz",
|
||||
"integrity": "sha512-CSfD8HO5lKCEklhkV/WjusWqiU4j8JQl7X69CfslESmkUQ+E9/clmzuUbYnEnvNaFQRbYvryfkht/SpirGb2iA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.78.0.tgz",
|
||||
"integrity": "sha512-cXDtu1U0LeZj/xfnFoV7yCze37TcbNo8FCxy1FpqhMbB9u9QxxDSW6pA5gm/07Ei7m260Lof4CZx67Cu6DPeig==",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"tslib": "2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.78.0.tgz",
|
||||
"integrity": "sha512-t1jOvArBsOINyqaRee1xJ3gryXLvkBzqnKfi6q3YRzzhJbGS6eXz0pXR5fqmJeB01fLC+1njpf3YhMszdPEF7g==",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"tslib": "2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.78.0.tgz",
|
||||
"integrity": "sha512-AwhpYlSvJ+PSnPmIK8sHj7NGDyDENYfQGKrMtpVIEzQA2ApUjgpUGxzXWN4Z0wEtLQsvv7g4y9HVad9Hzo1TNA==",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"tslib": "2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.78.0.tgz",
|
||||
"integrity": "sha512-rCs1zmLe7of7hj4s7G9z8rTqzWuNVtmwDr3FiCRCJFawEoa+RQO1xpZGbdeuVvVmKDyVN6b542Okci+117y/LQ==",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tslib": "2.8.1",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.78.0.tgz",
|
||||
"integrity": "sha512-n17P0JbjHOlxqJpkaGFOn97i3EusEKPEbWOpuk1r4t00Wg06B8Z4GUiq0O0n1vUpjiMgJUkLIMuBVp+bEgunzQ==",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"tslib": "2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.78.0.tgz",
|
||||
"integrity": "sha512-xYMRNBFmKp2m1gMuwcp/gr/HlfZKqjye1Ib8kJe29XJNsgwsfO/f8skxnWiscFKTlkOKLuBexNgl5L8dzGt6vA==",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.78.0",
|
||||
"@supabase/functions-js": "2.78.0",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "2.78.0",
|
||||
"@supabase/realtime-js": "2.78.0",
|
||||
"@supabase/storage-js": "2.78.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
@@ -2777,16 +2677,11 @@
|
||||
"version": "22.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz",
|
||||
"integrity": "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==",
|
||||
"peer": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A=="
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||
@@ -2799,14 +2694,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@@ -2850,7 +2737,6 @@
|
||||
"version": "1.35.6",
|
||||
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.35.6.tgz",
|
||||
"integrity": "sha512-aSEIGJgJ7XUfTYvhQcQbq835re7e/BXjs8Janq6Pvr6LlmTZnyqwT97RziZLO/8AVUL037RLXqqiQC6kCt+5pA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"uncrypto": "^0.1.3"
|
||||
}
|
||||
@@ -3066,6 +2952,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
@@ -3080,7 +2967,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3214,6 +3100,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
|
||||
"integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
@@ -3453,6 +3340,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
@@ -3526,7 +3414,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -3562,6 +3449,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -3570,6 +3458,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -3582,6 +3471,7 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
@@ -3860,6 +3750,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
@@ -3871,6 +3762,7 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -3903,6 +3795,7 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
@@ -4125,6 +4018,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -4177,6 +4071,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -4237,7 +4132,8 @@
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.244",
|
||||
@@ -4274,6 +4170,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -4312,6 +4209,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -4320,6 +4218,7 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -4335,6 +4234,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
@@ -4744,7 +4644,8 @@
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
@@ -4859,6 +4760,7 @@
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
@@ -4917,6 +4819,7 @@
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -5011,6 +4914,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
@@ -5074,6 +4978,7 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -5095,6 +5000,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -5151,6 +5057,7 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
@@ -5184,6 +5091,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
@@ -5243,6 +5151,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -5270,6 +5179,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -5312,6 +5222,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
@@ -5327,6 +5238,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -5364,6 +5276,7 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
@@ -5447,6 +5360,7 @@
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
@@ -5528,7 +5442,8 @@
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
@@ -6834,6 +6749,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -6847,6 +6763,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -6855,6 +6772,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -6889,117 +6807,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micro": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micro/-/micro-10.0.1.tgz",
|
||||
"integrity": "sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"arg": "4.1.0",
|
||||
"content-type": "1.0.4",
|
||||
"raw-body": "2.4.1"
|
||||
},
|
||||
"bin": {
|
||||
"micro": "dist/src/bin/micro.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/content-type": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
||||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/http-errors": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
||||
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/raw-body": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
||||
"integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.0",
|
||||
"http-errors": "1.7.3",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/micro/node_modules/statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micro/node_modules/toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -7044,6 +6851,7 @@
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -7052,6 +6860,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
@@ -7165,6 +6974,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -7431,6 +7241,7 @@
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -7442,6 +7253,7 @@
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
@@ -7654,6 +7466,7 @@
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
@@ -7757,7 +7570,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -7851,6 +7663,7 @@
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
@@ -7890,6 +7703,7 @@
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
@@ -7941,6 +7755,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
||||
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
@@ -7955,6 +7770,7 @@
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
@@ -7970,7 +7786,6 @@
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7979,7 +7794,6 @@
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -7990,14 +7804,12 @@
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"peer": true
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -8090,8 +7902,7 @@
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"peer": true
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -8199,7 +8010,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
||||
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -8240,6 +8050,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
@@ -8297,7 +8108,8 @@
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
@@ -8317,6 +8129,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.5",
|
||||
"encodeurl": "^2.0.0",
|
||||
@@ -8338,6 +8151,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
@@ -8356,7 +8170,8 @@
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
@@ -8385,6 +8200,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
@@ -8403,6 +8219,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
@@ -8418,6 +8235,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -8435,6 +8253,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -8541,6 +8360,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -8727,25 +8547,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.0.tgz",
|
||||
"integrity": "sha512-strzN8luMGMC1LEleGKg7pJGXFx0kSS4y/uSjK8yPQV9SUBMtJVAp/v8XMQLRnMbXaSaWLrIaHcMlKcsizdRDQ==",
|
||||
"dependencies": {
|
||||
"qs": "^6.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -8950,6 +8751,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
@@ -8957,7 +8759,8 @@
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-morph": {
|
||||
"version": "12.0.0",
|
||||
@@ -9053,6 +8856,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"media-typer": "^1.1.0",
|
||||
@@ -9111,7 +8915,8 @@
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
@@ -9140,6 +8945,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -9223,6 +9029,7 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -9253,7 +9060,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -9363,12 +9169,14 @@
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
@@ -9511,26 +9319,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
+1
-2
@@ -14,8 +14,7 @@
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@react-pdf/renderer": "^4.3.1",
|
||||
"@supabase/supabase-js": "^2.78.0",
|
||||
"@upstash/ratelimit": "^2.0.6",
|
||||
"@upstash/ratelimit": "^2.0.6",
|
||||
"@upstash/redis": "^1.35.6",
|
||||
"date-fns": "^4.1.0",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
formatCurrency,
|
||||
formatDate
|
||||
} from '../lib/invoices';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import type { Order, Subscription, User } from '../lib/supabase';
|
||||
import type { Order, Subscription, User } from '../lib/commerce';
|
||||
import type { InvoiceData } from '../lib/invoices';
|
||||
|
||||
interface DashboardInvoicesProps {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { supabase } from '../../lib/auth';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
@@ -27,8 +27,6 @@ const WebhookEventLog: React.FC = () => {
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
const [selectedEvent, setSelectedEvent] = useState<WebhookEvent | null>(null);
|
||||
|
||||
const supabase = createClient();
|
||||
|
||||
const eventTypes = [
|
||||
'all',
|
||||
'payment_intent.succeeded',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from './supabase';
|
||||
import { supabase } from './auth';
|
||||
import { startOfDay, endOfDay, subDays, format, eachDayOfInterval, eachWeekOfInterval, eachMonthOfInterval, startOfWeek, startOfMonth } from 'date-fns';
|
||||
|
||||
export interface RevenueDataPoint {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from './supabase';
|
||||
import { supabase } from './auth';
|
||||
import { generateInvoicePDF } from '../components/Invoice';
|
||||
import {
|
||||
createInvoiceFromOrder,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
storeInvoice,
|
||||
type InvoiceData
|
||||
} from './invoices';
|
||||
import type { Order, Subscription, User } from './supabase';
|
||||
import type { Order, Subscription, User } from './commerce';
|
||||
|
||||
/**
|
||||
* Generate and send invoice to customer
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { supabase } from './supabase';
|
||||
import { supabase } from './auth';
|
||||
|
||||
// Rate limit error class
|
||||
export class RateLimitError extends Error {
|
||||
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
// Hanzo IAM (hanzo.id) OAuth 2.0 PKCE client
|
||||
// Replaces Supabase auth entirely
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const IAM_URL = (import.meta.env.VITE_IAM_URL as string) || 'https://hanzo.id'
|
||||
const CLIENT_ID = (import.meta.env.VITE_IAM_CLIENT_ID as string) || 'app-computer'
|
||||
const REDIRECT_URI = `${window.location.origin}/auth/callback`
|
||||
|
||||
const AUTHORIZE_URL = `${IAM_URL}/login/oauth/authorize`
|
||||
const TOKEN_URL = `${IAM_URL}/api/login/oauth/access_token`
|
||||
const USERINFO_URL = `${IAM_URL}/api/userinfo`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Re-exported data types (unchanged from supabase.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
company?: string
|
||||
role: 'customer' | 'admin'
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface RFQ {
|
||||
id: string
|
||||
user_id?: string
|
||||
company: string
|
||||
email: string
|
||||
phone?: string
|
||||
gpu_type: string
|
||||
quantity: number
|
||||
duration_months?: number
|
||||
use_case: string
|
||||
budget_range?: string
|
||||
additional_requirements?: string
|
||||
status: 'pending' | 'reviewing' | 'quoted' | 'accepted' | 'rejected'
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ClusterRequest {
|
||||
id: string
|
||||
user_id?: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
company: string
|
||||
cluster_requirements: string
|
||||
number_of_gpus: string
|
||||
rental_duration: string
|
||||
project_description: string
|
||||
hear_about_us: string
|
||||
status: 'pending' | 'reviewing' | 'quoted' | 'accepted' | 'rejected'
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
id: string
|
||||
rfq_id?: string
|
||||
cluster_request_id?: string
|
||||
user_id?: string
|
||||
quote_number: string
|
||||
items: any // JSON
|
||||
subtotal: number
|
||||
tax: number
|
||||
total: number
|
||||
payment_terms?: string
|
||||
valid_until?: string
|
||||
notes?: string
|
||||
status: 'sent' | 'viewed' | 'accepted' | 'expired' | 'rejected'
|
||||
created_at: string
|
||||
accepted_at?: string
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string
|
||||
order_number: string
|
||||
user_id?: string
|
||||
quote_id?: string
|
||||
payment_intent_id?: string
|
||||
items: any // JSON
|
||||
subtotal: number
|
||||
tax: number
|
||||
total: number
|
||||
status: 'pending' | 'paid' | 'provisioning' | 'active' | 'cancelled'
|
||||
payment_method?: string
|
||||
billing_cycle?: string
|
||||
created_at: string
|
||||
paid_at?: string
|
||||
provisioned_at?: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: string
|
||||
user_id?: string
|
||||
order_id?: string
|
||||
subscription_id: string
|
||||
gpu_type: string
|
||||
quantity: number
|
||||
status: 'active' | 'cancelled' | 'past_due' | 'paused'
|
||||
current_period_start?: string
|
||||
current_period_end?: string
|
||||
cancel_at_period_end: boolean
|
||||
created_at: string
|
||||
cancelled_at?: string
|
||||
}
|
||||
|
||||
export interface UsageRecord {
|
||||
id: string
|
||||
user_id: string
|
||||
reservation_id?: string
|
||||
gpu_type: string
|
||||
hours_used: number
|
||||
compute_units: number
|
||||
cost_usd: number
|
||||
timestamp: string
|
||||
metadata?: any
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth user type (what we decode from the JWT / userinfo)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface AuthUser {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
preferred_username?: string
|
||||
avatar?: string
|
||||
roles?: string[]
|
||||
owner?: string // org claim
|
||||
}
|
||||
|
||||
export interface AuthSession {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
expires_at?: number
|
||||
user: AuthUser
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Storage keys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
ACCESS_TOKEN: 'hanzo_access_token',
|
||||
REFRESH_TOKEN: 'hanzo_refresh_token',
|
||||
EXPIRES_AT: 'hanzo_expires_at',
|
||||
CODE_VERIFIER: 'hanzo_code_verifier',
|
||||
} as const
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PKCE helpers (no dependencies -- uses Web Crypto API)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function generateRandomString(length: number): string {
|
||||
const array = new Uint8Array(length)
|
||||
crypto.getRandomValues(array)
|
||||
return Array.from(array, (b) => b.toString(36).padStart(2, '0'))
|
||||
.join('')
|
||||
.slice(0, length)
|
||||
}
|
||||
|
||||
async function sha256(plain: string): Promise<ArrayBuffer> {
|
||||
const encoder = new TextEncoder()
|
||||
return crypto.subtle.digest('SHA-256', encoder.encode(plain))
|
||||
}
|
||||
|
||||
function base64UrlEncode(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer)
|
||||
let binary = ''
|
||||
bytes.forEach((b) => (binary += String.fromCharCode(b)))
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
|
||||
async function generateCodeChallenge(verifier: string): Promise<string> {
|
||||
const digest = await sha256(verifier)
|
||||
return base64UrlEncode(digest)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JWT decoding (no validation -- server is the authority)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function decodeJwtPayload(token: string): Record<string, any> {
|
||||
const parts = token.split('.')
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid JWT format')
|
||||
}
|
||||
const payload = parts[1]
|
||||
// Re-add base64 padding
|
||||
const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4)
|
||||
const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'))
|
||||
return JSON.parse(decoded)
|
||||
}
|
||||
|
||||
function jwtToAuthUser(payload: Record<string, any>): AuthUser {
|
||||
return {
|
||||
id: payload.sub || payload.id || '',
|
||||
email: payload.email || '',
|
||||
name: payload.name || payload.preferred_username || payload.email || '',
|
||||
preferred_username: payload.preferred_username,
|
||||
avatar: payload.avatar || payload.picture,
|
||||
roles: payload.roles || payload.groups || [],
|
||||
owner: payload.owner || payload.org,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth state change listeners
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type AuthStateCallback = (
|
||||
event: 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED',
|
||||
session: AuthSession | null
|
||||
) => void
|
||||
|
||||
const listeners = new Set<AuthStateCallback>()
|
||||
|
||||
function notifyListeners(
|
||||
event: 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED',
|
||||
session: AuthSession | null
|
||||
): void {
|
||||
listeners.forEach((cb) => {
|
||||
try {
|
||||
cb(event, session)
|
||||
} catch (err) {
|
||||
console.error('[auth] listener error:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core auth functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Redirect to hanzo.id login with PKCE */
|
||||
export async function signIn(): Promise<void> {
|
||||
const codeVerifier = generateRandomString(64)
|
||||
sessionStorage.setItem(STORAGE_KEYS.CODE_VERIFIER, codeVerifier)
|
||||
|
||||
const codeChallenge = await generateCodeChallenge(codeVerifier)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
scope: 'openid profile email',
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
state: crypto.randomUUID(),
|
||||
})
|
||||
|
||||
window.location.href = `${AUTHORIZE_URL}?${params.toString()}`
|
||||
}
|
||||
|
||||
/** Redirect to hanzo.id signup page */
|
||||
export function signUp(): void {
|
||||
const params = new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
})
|
||||
|
||||
window.location.href = `${IAM_URL}/signup?${params.toString()}`
|
||||
}
|
||||
|
||||
/** Clear tokens and redirect to home */
|
||||
export function signOut(): void {
|
||||
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN)
|
||||
localStorage.removeItem(STORAGE_KEYS.EXPIRES_AT)
|
||||
sessionStorage.removeItem(STORAGE_KEYS.CODE_VERIFIER)
|
||||
|
||||
notifyListeners('SIGNED_OUT', null)
|
||||
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
/** Exchange authorization code for tokens (called from /auth/callback) */
|
||||
export async function handleCallback(code: string): Promise<AuthSession> {
|
||||
const codeVerifier = sessionStorage.getItem(STORAGE_KEYS.CODE_VERIFIER)
|
||||
if (!codeVerifier) {
|
||||
throw new Error('Missing PKCE code verifier. Auth flow may have expired.')
|
||||
}
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: CLIENT_ID,
|
||||
code,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
code_verifier: codeVerifier,
|
||||
})
|
||||
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
throw new Error(`Token exchange failed: ${response.status} ${text}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const accessToken: string = data.access_token
|
||||
const refreshToken: string | undefined = data.refresh_token
|
||||
const expiresIn: number = data.expires_in || 3600
|
||||
|
||||
// Store tokens
|
||||
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken)
|
||||
if (refreshToken) {
|
||||
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
|
||||
}
|
||||
const expiresAt = Date.now() + expiresIn * 1000
|
||||
localStorage.setItem(STORAGE_KEYS.EXPIRES_AT, String(expiresAt))
|
||||
|
||||
// Clean up verifier
|
||||
sessionStorage.removeItem(STORAGE_KEYS.CODE_VERIFIER)
|
||||
|
||||
const user = jwtToAuthUser(decodeJwtPayload(accessToken))
|
||||
const session: AuthSession = {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
expires_at: expiresAt,
|
||||
user,
|
||||
}
|
||||
|
||||
notifyListeners('SIGNED_IN', session)
|
||||
return session
|
||||
}
|
||||
|
||||
/** Return the current session from stored tokens, or null */
|
||||
export function getSession(): { data: { session: AuthSession | null } } {
|
||||
const accessToken = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
if (!accessToken) {
|
||||
return { data: { session: null } }
|
||||
}
|
||||
|
||||
const expiresAt = Number(localStorage.getItem(STORAGE_KEYS.EXPIRES_AT) || '0')
|
||||
if (expiresAt > 0 && Date.now() > expiresAt) {
|
||||
// Token expired -- clear and return null
|
||||
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN)
|
||||
localStorage.removeItem(STORAGE_KEYS.EXPIRES_AT)
|
||||
return { data: { session: null } }
|
||||
}
|
||||
|
||||
try {
|
||||
const user = jwtToAuthUser(decodeJwtPayload(accessToken))
|
||||
return {
|
||||
data: {
|
||||
session: {
|
||||
access_token: accessToken,
|
||||
refresh_token: localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN) || undefined,
|
||||
expires_at: expiresAt || undefined,
|
||||
user,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
return { data: { session: null } }
|
||||
}
|
||||
}
|
||||
|
||||
/** Decode JWT and return user info, or null if not signed in */
|
||||
export function getUser(): AuthUser | null {
|
||||
const accessToken = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
if (!accessToken) return null
|
||||
|
||||
try {
|
||||
return jwtToAuthUser(decodeJwtPayload(accessToken))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Register a callback for auth state changes. Returns an unsubscribe handle. */
|
||||
export function onAuthStateChange(
|
||||
callback: AuthStateCallback
|
||||
): { data: { subscription: { unsubscribe: () => void } } } {
|
||||
listeners.add(callback)
|
||||
|
||||
// Fire immediately with current state
|
||||
const { data: { session } } = getSession()
|
||||
if (session) {
|
||||
callback('SIGNED_IN', session)
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
subscription: {
|
||||
unsubscribe: () => {
|
||||
listeners.delete(callback)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if the current user has an admin role (from JWT claims) */
|
||||
export function isAdmin(): boolean {
|
||||
const user = getUser()
|
||||
if (!user) return false
|
||||
return (user.roles || []).some(
|
||||
(r) => r === 'admin' || r === 'org:admin' || r === 'role:admin'
|
||||
)
|
||||
}
|
||||
|
||||
/** Return the stored access token for API calls, or null */
|
||||
export function getAccessToken(): string | null {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
if (!token) return null
|
||||
|
||||
const expiresAt = Number(localStorage.getItem(STORAGE_KEYS.EXPIRES_AT) || '0')
|
||||
if (expiresAt > 0 && Date.now() > expiresAt) {
|
||||
return null
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
/** Fetch full user profile from the IAM userinfo endpoint */
|
||||
export async function fetchUserInfo(): Promise<AuthUser | null> {
|
||||
const token = getAccessToken()
|
||||
if (!token) return null
|
||||
|
||||
const response = await fetch(USERINFO_URL, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
if (!response.ok) return null
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
id: data.sub || data.id || '',
|
||||
email: data.email || '',
|
||||
name: data.name || data.preferred_username || data.email || '',
|
||||
preferred_username: data.preferred_username,
|
||||
avatar: data.avatar || data.picture,
|
||||
roles: data.roles || data.groups || [],
|
||||
owner: data.owner || data.org,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Supabase-compatible shim
|
||||
//
|
||||
// Provides supabase.auth.getSession(), supabase.auth.onAuthStateChange(), etc.
|
||||
// so existing page code can migrate incrementally.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const supabase = {
|
||||
// Stub for code that still references supabase.from() -- these queries
|
||||
// must be migrated to Commerce API endpoints. Calling .from() at runtime
|
||||
// will log a warning and return empty results so the UI degrades gracefully.
|
||||
from: (table: string) => {
|
||||
const warn = () => {
|
||||
console.warn(`[auth] supabase.from('${table}') is deprecated. Migrate to Commerce API.`)
|
||||
}
|
||||
const emptyQuery: any = {
|
||||
select: (..._args: any[]) => { warn(); return emptyQuery },
|
||||
insert: (..._args: any[]) => { warn(); return emptyQuery },
|
||||
update: (..._args: any[]) => { warn(); return emptyQuery },
|
||||
delete: (..._args: any[]) => { warn(); return emptyQuery },
|
||||
eq: (..._args: any[]) => emptyQuery,
|
||||
neq: (..._args: any[]) => emptyQuery,
|
||||
gt: (..._args: any[]) => emptyQuery,
|
||||
gte: (..._args: any[]) => emptyQuery,
|
||||
lt: (..._args: any[]) => emptyQuery,
|
||||
lte: (..._args: any[]) => emptyQuery,
|
||||
not: (..._args: any[]) => emptyQuery,
|
||||
in: (..._args: any[]) => emptyQuery,
|
||||
order: (..._args: any[]) => emptyQuery,
|
||||
limit: (..._args: any[]) => emptyQuery,
|
||||
single: () => Promise.resolve({ data: null, error: { message: `supabase.from('${table}') is deprecated` } }),
|
||||
then: (resolve: any) => resolve({ data: [], error: null, count: 0 }),
|
||||
}
|
||||
return emptyQuery
|
||||
},
|
||||
// Stub for supabase.storage calls -- returns no-op
|
||||
storage: {
|
||||
from: (bucket: string) => {
|
||||
const warn = () => {
|
||||
console.warn(`[auth] supabase.storage.from('${bucket}') is deprecated. Migrate to Commerce API.`)
|
||||
}
|
||||
return {
|
||||
upload: async (..._args: any[]) => { warn(); return { data: null, error: { message: 'storage deprecated' } } },
|
||||
getPublicUrl: (_path: string) => ({ data: { publicUrl: '' } }),
|
||||
createSignedUrl: async (_path: string, _expiresIn: number) => ({ data: { signedUrl: '' }, error: null }),
|
||||
}
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
getSession: async () => getSession(),
|
||||
onAuthStateChange: (callback: (event: string, session: any) => void) => {
|
||||
return onAuthStateChange((event, session) => {
|
||||
// Map to Supabase-style shape: session.user has { id, email, user_metadata }
|
||||
const mapped = session
|
||||
? {
|
||||
access_token: session.access_token,
|
||||
user: {
|
||||
id: session.user.id,
|
||||
email: session.user.email,
|
||||
user_metadata: {
|
||||
role: session.user.roles?.includes('admin') ? 'admin' : 'authenticated',
|
||||
name: session.user.name,
|
||||
},
|
||||
},
|
||||
}
|
||||
: null
|
||||
callback(event, mapped)
|
||||
})
|
||||
},
|
||||
signInWithPassword: async (_creds: { email: string; password: string }) => {
|
||||
// Password auth goes through the IAM OAuth flow -- redirect there
|
||||
await signIn()
|
||||
// This never returns because we redirect, but satisfy the type:
|
||||
return { data: { user: null }, error: null }
|
||||
},
|
||||
signInWithOAuth: async (_opts: { provider: string; options?: { redirectTo?: string } }) => {
|
||||
await signIn()
|
||||
return { error: null }
|
||||
},
|
||||
signUp: async (_opts: { email: string; password: string; options?: { data?: any } }) => {
|
||||
signUp()
|
||||
return { data: { user: null }, error: null }
|
||||
},
|
||||
resetPasswordForEmail: async (_email: string, _opts?: { redirectTo?: string }) => {
|
||||
// Redirect to IAM password reset
|
||||
window.location.href = `${IAM_URL}/forget-password`
|
||||
return { error: null }
|
||||
},
|
||||
signOut: async () => {
|
||||
signOut()
|
||||
return { error: null }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default export: auth object with all methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const auth = {
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
getSession,
|
||||
getUser,
|
||||
handleCallback,
|
||||
onAuthStateChange,
|
||||
isAdmin,
|
||||
getAccessToken,
|
||||
fetchUserInfo,
|
||||
}
|
||||
|
||||
export default auth
|
||||
@@ -0,0 +1,542 @@
|
||||
import auth from './auth';
|
||||
|
||||
// Commerce API configuration
|
||||
const COMMERCE_API_URL = import.meta.env.VITE_COMMERCE_API_URL || 'https://billing.hanzo.ai';
|
||||
|
||||
// ── Types (same as supabase.ts) ──────────────────────────────────────────────
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
company?: string;
|
||||
role: 'customer' | 'admin';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface RFQ {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
company: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
gpu_type: string;
|
||||
quantity: number;
|
||||
duration_months?: number;
|
||||
use_case: string;
|
||||
budget_range?: string;
|
||||
additional_requirements?: string;
|
||||
status: 'pending' | 'reviewing' | 'quoted' | 'accepted' | 'rejected';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ClusterRequest {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
cluster_requirements: string;
|
||||
number_of_gpus: string;
|
||||
rental_duration: string;
|
||||
project_description: string;
|
||||
hear_about_us: string;
|
||||
status: 'pending' | 'reviewing' | 'quoted' | 'accepted' | 'rejected';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
id: string;
|
||||
rfq_id?: string;
|
||||
cluster_request_id?: string;
|
||||
user_id?: string;
|
||||
quote_number: string;
|
||||
items: any;
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
total: number;
|
||||
payment_terms?: string;
|
||||
valid_until?: string;
|
||||
notes?: string;
|
||||
status: 'sent' | 'viewed' | 'accepted' | 'expired' | 'rejected';
|
||||
created_at: string;
|
||||
accepted_at?: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
order_number: string;
|
||||
user_id?: string;
|
||||
quote_id?: string;
|
||||
payment_intent_id?: string;
|
||||
items: any;
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
total: number;
|
||||
status: 'pending' | 'paid' | 'provisioning' | 'active' | 'cancelled';
|
||||
payment_method?: string;
|
||||
billing_cycle?: string;
|
||||
created_at: string;
|
||||
paid_at?: string;
|
||||
provisioned_at?: string;
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
order_id?: string;
|
||||
subscription_id: string;
|
||||
gpu_type: string;
|
||||
quantity: number;
|
||||
status: 'active' | 'cancelled' | 'past_due' | 'paused';
|
||||
current_period_start?: string;
|
||||
current_period_end?: string;
|
||||
cancel_at_period_end: boolean;
|
||||
created_at: string;
|
||||
cancelled_at?: string;
|
||||
}
|
||||
|
||||
export interface UsageRecord {
|
||||
id: string;
|
||||
user_id: string;
|
||||
reservation_id?: string;
|
||||
gpu_type: string;
|
||||
hours_used: number;
|
||||
compute_units: number;
|
||||
cost_usd: number;
|
||||
timestamp: string;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
// ── HTTP client ──────────────────────────────────────────────────────────────
|
||||
|
||||
async function commerceRequest<T>(
|
||||
path: string,
|
||||
options: { method?: string; body?: unknown } = {},
|
||||
): Promise<T> {
|
||||
const { method = 'GET', body } = options;
|
||||
const token = await auth.getAccessToken();
|
||||
|
||||
const url = `${COMMERCE_API_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (res.status === 404) {
|
||||
// Endpoint doesn't exist yet -- fail-open with null so callers can
|
||||
// return default/empty data.
|
||||
return null as T;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => '');
|
||||
throw new Error(`Commerce API ${method} ${path} failed (${res.status}): ${detail}`);
|
||||
}
|
||||
|
||||
// 204 No Content or empty body
|
||||
const text = await res.text();
|
||||
if (!text) return null as T;
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
// ── RFQ functions ────────────────────────────────────────────────────────────
|
||||
|
||||
export const submitRFQ = async (
|
||||
rfqData: Omit<RFQ, 'id' | 'created_at' | 'updated_at' | 'status'>,
|
||||
): Promise<RFQ> => {
|
||||
try {
|
||||
const data = await commerceRequest<RFQ>('/api/v1/order', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
metadata: { type: 'rfq', ...rfqData },
|
||||
status: 'open',
|
||||
},
|
||||
});
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('submitRFQ failed:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserRFQs = async (userId: string): Promise<RFQ[]> => {
|
||||
try {
|
||||
// Try metadata filter first; if Commerce doesn't support it, fetch all
|
||||
// orders and filter client-side.
|
||||
const data = await commerceRequest<any[]>(
|
||||
`/api/v1/order?userId=${encodeURIComponent(userId)}&metadata.type=rfq`,
|
||||
);
|
||||
if (data) return data as RFQ[];
|
||||
|
||||
// Fallback: fetch all user orders and filter.
|
||||
const allOrders = await commerceRequest<any[]>(
|
||||
`/api/v1/order?userId=${encodeURIComponent(userId)}`,
|
||||
);
|
||||
if (!allOrders) return [];
|
||||
return allOrders.filter((o) => o.metadata?.type === 'rfq') as RFQ[];
|
||||
} catch (err) {
|
||||
console.error('getUserRFQs failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const submitClusterRequest = async (
|
||||
clusterData: Omit<ClusterRequest, 'id' | 'created_at' | 'updated_at' | 'status'>,
|
||||
): Promise<ClusterRequest> => {
|
||||
try {
|
||||
const data = await commerceRequest<ClusterRequest>('/api/v1/order', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
metadata: { type: 'cluster_request', ...clusterData },
|
||||
status: 'open',
|
||||
},
|
||||
});
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('submitClusterRequest failed:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllClusterRequests = async (status?: string): Promise<ClusterRequest[]> => {
|
||||
try {
|
||||
let path = '/api/v1/order?metadata.type=cluster_request';
|
||||
if (status) {
|
||||
path += `&status=${encodeURIComponent(status)}`;
|
||||
}
|
||||
|
||||
const data = await commerceRequest<any[]>(path);
|
||||
if (data) return data as ClusterRequest[];
|
||||
|
||||
// Fallback: fetch all orders and filter client-side.
|
||||
const allOrders = await commerceRequest<any[]>('/api/v1/order');
|
||||
if (!allOrders) return [];
|
||||
let filtered = allOrders.filter((o) => o.metadata?.type === 'cluster_request');
|
||||
if (status) {
|
||||
filtered = filtered.filter((o) => o.status === status);
|
||||
}
|
||||
return filtered as ClusterRequest[];
|
||||
} catch (err) {
|
||||
console.error('getAllClusterRequests failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// ── Quote functions ──────────────────────────────────────────────────────────
|
||||
|
||||
export const getUserQuotes = async (userId: string): Promise<Quote[]> => {
|
||||
try {
|
||||
const data = await commerceRequest<Quote[]>(
|
||||
`/api/v1/billing/invoices?userId=${encodeURIComponent(userId)}&status=draft`,
|
||||
);
|
||||
return data ?? [];
|
||||
} catch (err) {
|
||||
console.error('getUserQuotes failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const createQuote = async (
|
||||
quoteData: Omit<Quote, 'id' | 'created_at' | 'status'>,
|
||||
): Promise<Quote> => {
|
||||
try {
|
||||
const data = await commerceRequest<Quote>('/api/v1/billing/invoices', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
...quoteData,
|
||||
status: 'draft',
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('createQuote failed:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllQuotes = async (status?: string): Promise<Quote[]> => {
|
||||
try {
|
||||
let path = '/api/v1/billing/invoices?status=draft';
|
||||
if (status) {
|
||||
path = `/api/v1/billing/invoices?status=${encodeURIComponent(status)}`;
|
||||
}
|
||||
const data = await commerceRequest<Quote[]>(path);
|
||||
return data ?? [];
|
||||
} catch (err) {
|
||||
console.error('getAllQuotes failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const updateQuoteStatus = async (
|
||||
id: string,
|
||||
status: Quote['status'],
|
||||
): Promise<Quote> => {
|
||||
try {
|
||||
const data = await commerceRequest<Quote>(
|
||||
`/api/v1/billing/invoices/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: { status },
|
||||
},
|
||||
);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('updateQuoteStatus failed:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const generateQuoteNumber = (): string => {
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
return `Q${year}${month}-${random}`;
|
||||
};
|
||||
|
||||
// ── Order functions ──────────────────────────────────────────────────────────
|
||||
|
||||
export const getUserOrders = async (userId: string): Promise<Order[]> => {
|
||||
try {
|
||||
const data = await commerceRequest<Order[]>(
|
||||
`/api/v1/order?userId=${encodeURIComponent(userId)}`,
|
||||
);
|
||||
return data ?? [];
|
||||
} catch (err) {
|
||||
console.error('getUserOrders failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllRFQs = async (status?: string): Promise<RFQ[]> => {
|
||||
try {
|
||||
let path = '/api/v1/order?metadata.type=rfq';
|
||||
if (status) {
|
||||
path += `&status=${encodeURIComponent(status)}`;
|
||||
}
|
||||
|
||||
const data = await commerceRequest<any[]>(path);
|
||||
if (data) return data as RFQ[];
|
||||
|
||||
// Fallback: fetch all orders and filter client-side.
|
||||
const allOrders = await commerceRequest<any[]>('/api/v1/order');
|
||||
if (!allOrders) return [];
|
||||
let filtered = allOrders.filter((o) => o.metadata?.type === 'rfq');
|
||||
if (status) {
|
||||
filtered = filtered.filter((o) => o.status === status);
|
||||
}
|
||||
return filtered as RFQ[];
|
||||
} catch (err) {
|
||||
console.error('getAllRFQs failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRFQStatus = async (
|
||||
id: string,
|
||||
status: RFQ['status'],
|
||||
): Promise<RFQ> => {
|
||||
try {
|
||||
const data = await commerceRequest<RFQ>(
|
||||
`/api/v1/order/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: { status, updated_at: new Date().toISOString() },
|
||||
},
|
||||
);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('updateRFQStatus failed:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClusterRequestStatus = async (
|
||||
id: string,
|
||||
status: ClusterRequest['status'],
|
||||
): Promise<ClusterRequest> => {
|
||||
try {
|
||||
const data = await commerceRequest<ClusterRequest>(
|
||||
`/api/v1/order/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: { status, updated_at: new Date().toISOString() },
|
||||
},
|
||||
);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('updateClusterRequestStatus failed:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Subscription functions ───────────────────────────────────────────────────
|
||||
|
||||
export const getUserSubscriptions = async (userId: string): Promise<Subscription[]> => {
|
||||
try {
|
||||
const data = await commerceRequest<Subscription[]>(
|
||||
`/api/v1/billing/subscriptions?userId=${encodeURIComponent(userId)}`,
|
||||
);
|
||||
return data ?? [];
|
||||
} catch (err) {
|
||||
console.error('getUserSubscriptions failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// ── Usage / Analytics functions ──────────────────────────────────────────────
|
||||
|
||||
export const getUserUsage = async (
|
||||
userId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): Promise<UsageRecord[]> => {
|
||||
try {
|
||||
const data = await commerceRequest<UsageRecord[]>(
|
||||
`/api/v1/billing/meter-events/summary?userId=${encodeURIComponent(userId)}` +
|
||||
`&start=${encodeURIComponent(startDate.toISOString())}` +
|
||||
`&end=${encodeURIComponent(endDate.toISOString())}`,
|
||||
);
|
||||
return data ?? [];
|
||||
} catch (err) {
|
||||
console.error('getUserUsage failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getUsageSummary = async (userId: string) => {
|
||||
try {
|
||||
const data = await commerceRequest<any[]>(
|
||||
`/api/v1/billing/meter-events/summary?userId=${encodeURIComponent(userId)}&dimensions=gpu_type`,
|
||||
);
|
||||
|
||||
const summary = {
|
||||
totalHours: 0,
|
||||
totalCost: 0,
|
||||
totalComputeUnits: 0,
|
||||
byGpuType: {} as Record<string, { hours: number; cost: number; count: number }>,
|
||||
};
|
||||
|
||||
if (!data) return summary;
|
||||
|
||||
for (const record of data) {
|
||||
summary.totalHours += record.hours_used ?? 0;
|
||||
summary.totalCost += record.cost_usd ?? 0;
|
||||
summary.totalComputeUnits += record.compute_units ?? 0;
|
||||
|
||||
const gpuType = record.gpu_type;
|
||||
if (gpuType) {
|
||||
if (!summary.byGpuType[gpuType]) {
|
||||
summary.byGpuType[gpuType] = { hours: 0, cost: 0, count: 0 };
|
||||
}
|
||||
summary.byGpuType[gpuType].hours += record.hours_used ?? 0;
|
||||
summary.byGpuType[gpuType].cost += record.cost_usd ?? 0;
|
||||
summary.byGpuType[gpuType].count++;
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
} catch (err) {
|
||||
console.error('getUsageSummary failed:', err);
|
||||
return {
|
||||
totalHours: 0,
|
||||
totalCost: 0,
|
||||
totalComputeUnits: 0,
|
||||
byGpuType: {} as Record<string, { hours: number; cost: number; count: number }>,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getMonthlySpending = async (
|
||||
userId: string,
|
||||
months: number = 6,
|
||||
): Promise<{ month: string; cost: number }[]> => {
|
||||
try {
|
||||
const data = await commerceRequest<any[]>(
|
||||
`/api/v1/billing/invoices?userId=${encodeURIComponent(userId)}&period=last_${months}_months`,
|
||||
);
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
// Group by month
|
||||
const monthlySpending: Record<string, number> = {};
|
||||
|
||||
for (const record of data) {
|
||||
const ts = record.timestamp || record.created_at;
|
||||
if (!ts) continue;
|
||||
const date = new Date(ts);
|
||||
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
monthlySpending[monthKey] = (monthlySpending[monthKey] ?? 0) + (record.cost_usd ?? record.total ?? 0);
|
||||
}
|
||||
|
||||
return Object.entries(monthlySpending).map(([month, cost]) => ({ month, cost }));
|
||||
} catch (err) {
|
||||
console.error('getMonthlySpending failed:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getGPUUtilization = async (
|
||||
userId: string,
|
||||
): Promise<Record<string, number>> => {
|
||||
try {
|
||||
const records = await getUserUsage(
|
||||
userId,
|
||||
new Date(0), // all-time
|
||||
new Date(),
|
||||
);
|
||||
|
||||
const utilization: Record<string, number> = {};
|
||||
|
||||
for (const record of records) {
|
||||
utilization[record.gpu_type] = (utilization[record.gpu_type] ?? 0) + record.hours_used;
|
||||
}
|
||||
|
||||
return utilization;
|
||||
} catch (err) {
|
||||
console.error('getGPUUtilization failed:', err);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const saveUsageRecord = async (
|
||||
record: Omit<UsageRecord, 'id'>,
|
||||
): Promise<UsageRecord> => {
|
||||
try {
|
||||
const data = await commerceRequest<UsageRecord>('/api/v1/billing/meter-events', {
|
||||
method: 'POST',
|
||||
body: record,
|
||||
});
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('saveUsageRecord failed:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Admin functions ──────────────────────────────────────────────────────────
|
||||
|
||||
export const checkAdminRole = async (_userId: string): Promise<boolean> => {
|
||||
try {
|
||||
return await auth.isAdmin();
|
||||
} catch (err) {
|
||||
console.error('checkAdminRole failed:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { supabase } from './supabase';
|
||||
import type { Order, User, Subscription } from './supabase';
|
||||
import { supabase } from './auth';
|
||||
import type { Order, User, Subscription } from './commerce';
|
||||
|
||||
export interface InvoiceLineItem {
|
||||
description: string;
|
||||
|
||||
@@ -1,421 +0,0 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
// Supabase configuration
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || '';
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || '';
|
||||
|
||||
// Create Supabase client
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
// Database types (will be auto-generated from Supabase later)
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
company?: string;
|
||||
role: 'customer' | 'admin';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface RFQ {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
company: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
gpu_type: string;
|
||||
quantity: number;
|
||||
duration_months?: number;
|
||||
use_case: string;
|
||||
budget_range?: string;
|
||||
additional_requirements?: string;
|
||||
status: 'pending' | 'reviewing' | 'quoted' | 'accepted' | 'rejected';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ClusterRequest {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
cluster_requirements: string;
|
||||
number_of_gpus: string;
|
||||
rental_duration: string;
|
||||
project_description: string;
|
||||
hear_about_us: string;
|
||||
status: 'pending' | 'reviewing' | 'quoted' | 'accepted' | 'rejected';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
id: string;
|
||||
rfq_id?: string;
|
||||
cluster_request_id?: string;
|
||||
user_id?: string;
|
||||
quote_number: string;
|
||||
items: any; // JSON
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
total: number;
|
||||
payment_terms?: string;
|
||||
valid_until?: string;
|
||||
notes?: string;
|
||||
status: 'sent' | 'viewed' | 'accepted' | 'expired' | 'rejected';
|
||||
created_at: string;
|
||||
accepted_at?: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
order_number: string;
|
||||
user_id?: string;
|
||||
quote_id?: string;
|
||||
payment_intent_id?: string;
|
||||
items: any; // JSON
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
total: number;
|
||||
status: 'pending' | 'paid' | 'provisioning' | 'active' | 'cancelled';
|
||||
payment_method?: string;
|
||||
billing_cycle?: string;
|
||||
created_at: string;
|
||||
paid_at?: string;
|
||||
provisioned_at?: string;
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
order_id?: string;
|
||||
subscription_id: string;
|
||||
gpu_type: string;
|
||||
quantity: number;
|
||||
status: 'active' | 'cancelled' | 'past_due' | 'paused';
|
||||
current_period_start?: string;
|
||||
current_period_end?: string;
|
||||
cancel_at_period_end: boolean;
|
||||
created_at: string;
|
||||
cancelled_at?: string;
|
||||
}
|
||||
|
||||
export interface UsageRecord {
|
||||
id: string;
|
||||
user_id: string;
|
||||
reservation_id?: string;
|
||||
gpu_type: string;
|
||||
hours_used: number;
|
||||
compute_units: number;
|
||||
cost_usd: number;
|
||||
timestamp: string;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
export const submitRFQ = async (rfqData: Omit<RFQ, 'id' | 'created_at' | 'updated_at' | 'status'>) => {
|
||||
const { data, error } = await supabase
|
||||
.from('rfqs')
|
||||
.insert([{
|
||||
...rfqData,
|
||||
status: 'pending',
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const submitClusterRequest = async (clusterData: Omit<ClusterRequest, 'id' | 'created_at' | 'updated_at' | 'status'>) => {
|
||||
const { data, error } = await supabase
|
||||
.from('cluster_requests')
|
||||
.insert([{
|
||||
...clusterData,
|
||||
status: 'pending',
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getUserRFQs = async (userId: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('rfqs')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getUserQuotes = async (userId: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('quotes')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getUserOrders = async (userId: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('orders')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getUserSubscriptions = async (userId: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
// Admin functions
|
||||
export const getAllRFQs = async (status?: string) => {
|
||||
let query = supabase
|
||||
.from('rfqs')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq('status', status);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getAllClusterRequests = async (status?: string) => {
|
||||
let query = supabase
|
||||
.from('cluster_requests')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq('status', status);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateRFQStatus = async (id: string, status: RFQ['status']) => {
|
||||
const { data, error } = await supabase
|
||||
.from('rfqs')
|
||||
.update({ status, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateClusterRequestStatus = async (id: string, status: ClusterRequest['status']) => {
|
||||
const { data, error } = await supabase
|
||||
.from('cluster_requests')
|
||||
.update({ status, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
// Quote management functions
|
||||
export const createQuote = async (quoteData: Omit<Quote, 'id' | 'created_at' | 'status'>) => {
|
||||
const { data, error } = await supabase
|
||||
.from('quotes')
|
||||
.insert([{
|
||||
...quoteData,
|
||||
status: 'sent',
|
||||
created_at: new Date().toISOString(),
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getAllQuotes = async (status?: string) => {
|
||||
let query = supabase
|
||||
.from('quotes')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq('status', status);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateQuoteStatus = async (id: string, status: Quote['status']) => {
|
||||
const { data, error } = await supabase
|
||||
.from('quotes')
|
||||
.update({ status })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
// Generate unique quote number
|
||||
export const generateQuoteNumber = () => {
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
return `Q${year}${month}-${random}`;
|
||||
};
|
||||
|
||||
// Check if user is admin
|
||||
export const checkAdminRole = async (userId: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('users')
|
||||
.select('role')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data?.role === 'admin';
|
||||
};
|
||||
|
||||
// Usage Analytics Functions
|
||||
export const getUserUsage = async (userId: string, startDate: Date, endDate: Date) => {
|
||||
const { data, error } = await supabase
|
||||
.from('usage_records')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.gte('timestamp', startDate.toISOString())
|
||||
.lte('timestamp', endDate.toISOString())
|
||||
.order('timestamp', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getUsageSummary = async (userId: string) => {
|
||||
const now = new Date();
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('usage_records')
|
||||
.select('gpu_type, hours_used, compute_units, cost_usd')
|
||||
.eq('user_id', userId)
|
||||
.gte('timestamp', startOfMonth.toISOString());
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const summary = {
|
||||
totalHours: 0,
|
||||
totalCost: 0,
|
||||
totalComputeUnits: 0,
|
||||
byGpuType: {} as Record<string, { hours: number; cost: number; count: number }>
|
||||
};
|
||||
|
||||
data?.forEach(record => {
|
||||
summary.totalHours += record.hours_used;
|
||||
summary.totalCost += record.cost_usd;
|
||||
summary.totalComputeUnits += record.compute_units;
|
||||
|
||||
if (!summary.byGpuType[record.gpu_type]) {
|
||||
summary.byGpuType[record.gpu_type] = { hours: 0, cost: 0, count: 0 };
|
||||
}
|
||||
summary.byGpuType[record.gpu_type].hours += record.hours_used;
|
||||
summary.byGpuType[record.gpu_type].cost += record.cost_usd;
|
||||
summary.byGpuType[record.gpu_type].count++;
|
||||
});
|
||||
|
||||
return summary;
|
||||
};
|
||||
|
||||
export const getMonthlySpending = async (userId: string, months: number = 6) => {
|
||||
const now = new Date();
|
||||
const startDate = new Date(now.getFullYear(), now.getMonth() - months + 1, 1);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('usage_records')
|
||||
.select('cost_usd, timestamp')
|
||||
.eq('user_id', userId)
|
||||
.gte('timestamp', startDate.toISOString())
|
||||
.order('timestamp', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Group by month
|
||||
const monthlySpending: Record<string, number> = {};
|
||||
|
||||
data?.forEach(record => {
|
||||
const date = new Date(record.timestamp);
|
||||
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
if (!monthlySpending[monthKey]) {
|
||||
monthlySpending[monthKey] = 0;
|
||||
}
|
||||
monthlySpending[monthKey] += record.cost_usd;
|
||||
});
|
||||
|
||||
return Object.entries(monthlySpending).map(([month, cost]) => ({
|
||||
month,
|
||||
cost
|
||||
}));
|
||||
};
|
||||
|
||||
export const getGPUUtilization = async (userId: string) => {
|
||||
const { data, error } = await supabase
|
||||
.from('usage_records')
|
||||
.select('gpu_type, hours_used')
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const utilization: Record<string, number> = {};
|
||||
|
||||
data?.forEach(record => {
|
||||
if (!utilization[record.gpu_type]) {
|
||||
utilization[record.gpu_type] = 0;
|
||||
}
|
||||
utilization[record.gpu_type] += record.hours_used;
|
||||
});
|
||||
|
||||
return utilization;
|
||||
};
|
||||
|
||||
// Save usage record to database
|
||||
export const saveUsageRecord = async (record: Omit<UsageRecord, 'id'>) => {
|
||||
const { data, error } = await supabase
|
||||
.from('usage_records')
|
||||
.insert([record])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { supabase } from '../lib/auth';
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
|
||||
@@ -46,8 +46,8 @@ import {
|
||||
Funnel as RechartsFunction,
|
||||
FunnelChart
|
||||
} from 'recharts';
|
||||
import { getUser } from '../lib/auth';
|
||||
import {
|
||||
supabase,
|
||||
checkAdminRole,
|
||||
getAllRFQs,
|
||||
getAllClusterRequests,
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
type RFQ,
|
||||
type ClusterRequest,
|
||||
type Quote
|
||||
} from '../lib/supabase';
|
||||
} from '../lib/commerce';
|
||||
import {
|
||||
getRevenueData,
|
||||
getConversionFunnel,
|
||||
@@ -252,7 +252,7 @@ const AdminDashboard: React.FC = () => {
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const user = getUser();
|
||||
|
||||
if (!user) {
|
||||
navigate('/signin');
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
generateSampleData,
|
||||
type UsageStats
|
||||
} from '../lib/analytics';
|
||||
import { getUserUsage, getUsageSummary, getMonthlySpending } from '../lib/supabase';
|
||||
import { getUserUsage, getUsageSummary, getMonthlySpending } from '../lib/commerce';
|
||||
|
||||
// Color palette for charts
|
||||
const COLORS = ['#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899', '#6366f1'];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { handleCallback } from '../lib/auth';
|
||||
|
||||
const AuthCallback: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const code = searchParams.get('code');
|
||||
if (!code) {
|
||||
setError('No authorization code received');
|
||||
return;
|
||||
}
|
||||
|
||||
handleCallback(code)
|
||||
.then(() => {
|
||||
navigate('/dashboard', { replace: true });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Auth callback failed:', err);
|
||||
setError(err.message || 'Authentication failed');
|
||||
});
|
||||
}, [searchParams, navigate]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-white mb-4">Authentication Error</h1>
|
||||
<p className="text-red-400 mb-6">{error}</p>
|
||||
<a href="/signin" className="text-primary hover:underline">
|
||||
Try signing in again
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
<p className="text-gray-400">Completing sign in...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthCallback;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { getUserOrders } from '../lib/commerce';
|
||||
import { getUser } from '../lib/auth';
|
||||
|
||||
interface OrderDetails {
|
||||
id: string;
|
||||
@@ -32,17 +33,23 @@ const CheckoutSuccess: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// First try to fetch from Supabase
|
||||
const { data: orderData, error } = await supabase
|
||||
.from('orders')
|
||||
.select('*')
|
||||
.eq('payment_intent_id', paymentIntentId)
|
||||
.single();
|
||||
|
||||
if (!error && orderData) {
|
||||
setOrder(orderData);
|
||||
// Fetch from Commerce API
|
||||
const user = getUser();
|
||||
if (user) {
|
||||
const allOrders = await getUserOrders(user.id);
|
||||
const matched = allOrders.find((o: any) => o.payment_intent_id === paymentIntentId);
|
||||
if (matched) {
|
||||
setOrder(matched as any);
|
||||
} else {
|
||||
// Fallback to localStorage
|
||||
const orders = JSON.parse(localStorage.getItem('orders') || '[]');
|
||||
const localOrder = orders.find((o: any) => o.payment_intent_id === paymentIntentId);
|
||||
if (localOrder) {
|
||||
setOrder(localOrder);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to localStorage
|
||||
// Not signed in -- fallback to localStorage
|
||||
const orders = JSON.parse(localStorage.getItem('orders') || '[]');
|
||||
const localOrder = orders.find((o: any) => o.payment_intent_id === paymentIntentId);
|
||||
if (localOrder) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { submitClusterRequest } from '../lib/supabase';
|
||||
import { submitClusterRequest } from '../lib/commerce';
|
||||
|
||||
interface ClusterFormData {
|
||||
firstName: string;
|
||||
|
||||
+27
-48
@@ -1,7 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { supabase, getUserRFQs, getUserQuotes, getUserOrders, getUserSubscriptions } from '../lib/supabase';
|
||||
import type { RFQ, Quote, Order, Subscription, User } from '../lib/supabase';
|
||||
import auth, { supabase } from '../lib/auth';
|
||||
import type { User } from '../lib/auth';
|
||||
import { getUserRFQs, getUserQuotes, getUserOrders, getUserSubscriptions, updateQuoteStatus } from '../lib/commerce';
|
||||
import type { RFQ, Quote, Order, Subscription } from '../lib/commerce';
|
||||
import DashboardInvoices from '../components/DashboardInvoices';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
@@ -38,15 +40,17 @@ const Dashboard: React.FC = () => {
|
||||
const loadDashboardData = async (userId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Fetch user details first
|
||||
const { data: userData, error: userError } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
if (!userError && userData) {
|
||||
setUserDetails(userData);
|
||||
// Get user details from auth
|
||||
const authUser = await auth.fetchUserInfo();
|
||||
if (authUser) {
|
||||
setUserDetails({
|
||||
id: authUser.id,
|
||||
email: authUser.email,
|
||||
name: authUser.name,
|
||||
role: authUser.roles?.includes('admin') ? 'admin' : 'customer',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
} as User);
|
||||
}
|
||||
|
||||
const [rfqsData, quotesData, ordersData, subscriptionsData] = await Promise.all([
|
||||
@@ -73,28 +77,7 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
const handleAcceptQuote = async (quoteId: string) => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('quotes')
|
||||
.update({ status: 'accepted' })
|
||||
.eq('id', quoteId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Create an order from the accepted quote
|
||||
const quote = quotes.find(q => q.id === quoteId);
|
||||
if (quote) {
|
||||
const orderNumber = `ORD-${Date.now()}`;
|
||||
const { error: orderError } = await supabase
|
||||
.from('orders')
|
||||
.insert({
|
||||
quote_id: quoteId,
|
||||
order_number: orderNumber,
|
||||
total: quote.total,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
if (orderError) throw orderError;
|
||||
}
|
||||
await updateQuoteStatus(quoteId, 'accepted');
|
||||
|
||||
// Reload dashboard data
|
||||
if (user) {
|
||||
@@ -108,12 +91,7 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
const handleRejectQuote = async (quoteId: string) => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('quotes')
|
||||
.update({ status: 'rejected' })
|
||||
.eq('id', quoteId);
|
||||
|
||||
if (error) throw error;
|
||||
await updateQuoteStatus(quoteId, 'rejected');
|
||||
|
||||
// Reload dashboard data
|
||||
if (user) {
|
||||
@@ -131,15 +109,16 @@ const Dashboard: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('subscriptions')
|
||||
.update({
|
||||
status: 'cancelled',
|
||||
cancelled_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', subscriptionId);
|
||||
|
||||
if (error) throw error;
|
||||
// Cancel via Commerce API
|
||||
const token = localStorage.getItem('hanzo_access_token');
|
||||
const commerceUrl = import.meta.env.VITE_COMMERCE_API_URL || 'https://billing.hanzo.ai';
|
||||
await fetch(`${commerceUrl}/api/v1/billing/subscriptions/${subscriptionId}/cancel`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Reload dashboard data
|
||||
if (user) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { submitRFQ } from '../lib/supabase';
|
||||
import { submitRFQ } from '../lib/commerce';
|
||||
import { sendRFQConfirmationClient, generateId, formatDate } from '../lib/email-client';
|
||||
import type { RFQData } from '../lib/email';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { supabase } from '../lib/auth';
|
||||
import { Mail, Lock, AlertCircle, Loader2 } from 'lucide-react';
|
||||
|
||||
const SignIn: React.FC = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { supabase } from '../lib/auth';
|
||||
import { Mail, Lock, User, Building, Phone, AlertCircle, Loader2, Check } from 'lucide-react';
|
||||
|
||||
const SignUp: React.FC = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { supabase } from '../../lib/auth';
|
||||
import { Shield, AlertTriangle, TrendingUp, Users, Activity, Clock, Settings, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface RateLimitStats {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
// Test script to verify analytics functions are working
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = process.env.VITE_SUPABASE_URL || 'https://your-project.supabase.co';
|
||||
const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY || 'your-anon-key';
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
async function testAnalytics() {
|
||||
console.log('Testing Analytics Functions...\n');
|
||||
|
||||
// Test 1: Check if tables exist
|
||||
console.log('1. Checking database tables...');
|
||||
try {
|
||||
const { data: rfqs, error: rfqError } = await supabase
|
||||
.from('rfqs')
|
||||
.select('count', { count: 'exact', head: true });
|
||||
|
||||
const { data: quotes, error: quoteError } = await supabase
|
||||
.from('quotes')
|
||||
.select('count', { count: 'exact', head: true });
|
||||
|
||||
const { data: users, error: userError } = await supabase
|
||||
.from('users')
|
||||
.select('count', { count: 'exact', head: true });
|
||||
|
||||
console.log('✓ Database tables accessible');
|
||||
console.log(` - RFQs table: ${rfqError ? 'Error' : 'OK'}`);
|
||||
console.log(` - Quotes table: ${quoteError ? 'Error' : 'OK'}`);
|
||||
console.log(` - Users table: ${userError ? 'Error' : 'OK'}`);
|
||||
} catch (error) {
|
||||
console.error('✗ Database connection error:', error.message);
|
||||
}
|
||||
|
||||
// Test 2: Check data aggregation
|
||||
console.log('\n2. Testing data aggregation...');
|
||||
try {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - 30);
|
||||
|
||||
const { data: revenueData, error } = await supabase
|
||||
.from('quotes')
|
||||
.select('created_at, total, gpu_type, status')
|
||||
.eq('status', 'accepted')
|
||||
.gte('created_at', startDate.toISOString())
|
||||
.lte('created_at', endDate.toISOString());
|
||||
|
||||
if (!error) {
|
||||
const totalRevenue = revenueData?.reduce((sum, order) => sum + order.total, 0) || 0;
|
||||
console.log('✓ Revenue calculation working');
|
||||
console.log(` - Total revenue (30 days): $${totalRevenue.toLocaleString()}`);
|
||||
console.log(` - Orders processed: ${revenueData?.length || 0}`);
|
||||
} else {
|
||||
console.log('✗ Revenue calculation error:', error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('✗ Aggregation error:', error.message);
|
||||
}
|
||||
|
||||
// Test 3: Check chart data formatting
|
||||
console.log('\n3. Testing chart data formatting...');
|
||||
try {
|
||||
const mockData = [
|
||||
{ date: '2024-10-01', revenue: 15000, orders: 3 },
|
||||
{ date: '2024-10-02', revenue: 22000, orders: 5 },
|
||||
{ date: '2024-10-03', revenue: 18500, orders: 4 },
|
||||
];
|
||||
|
||||
console.log('✓ Chart data structure valid');
|
||||
console.log(' Sample data point:', JSON.stringify(mockData[0], null, 2));
|
||||
} catch (error) {
|
||||
console.error('✗ Data formatting error:', error.message);
|
||||
}
|
||||
|
||||
// Test 4: Check conversion funnel
|
||||
console.log('\n4. Testing conversion funnel...');
|
||||
try {
|
||||
const funnel = [
|
||||
{ stage: 'RFQs Submitted', value: 100, percentage: 100 },
|
||||
{ stage: 'Quotes Sent', value: 75, percentage: 75 },
|
||||
{ stage: 'Orders Placed', value: 25, percentage: 25 },
|
||||
];
|
||||
|
||||
console.log('✓ Conversion funnel structure valid');
|
||||
console.log(' Conversion rate: 25%');
|
||||
} catch (error) {
|
||||
console.error('✗ Funnel calculation error:', error.message);
|
||||
}
|
||||
|
||||
console.log('\n✅ Analytics tests completed!\n');
|
||||
}
|
||||
|
||||
// Run tests
|
||||
testAnalytics().catch(console.error);
|
||||
Reference in New Issue
Block a user