mirror of
https://github.com/hanzoai/computer.git
synced 2026-09-30 20:55:46 +00:00
Add complete Supabase integration for data persistence and authentication: Backend Setup: - Created Supabase client configuration (src/lib/supabase.ts) - Defined TypeScript interfaces for all database tables - Implemented helper functions for RFQ and cluster request submission - Added functions for customer data retrieval (quotes, orders, subscriptions) - Added admin functions for managing all requests Database Schema (supabase/schema.sql): - 10 tables: users, rfqs, cluster_requests, quotes, orders, subscriptions, invoices, gpu_reservations, usage_records, admin_actions - Row Level Security (RLS) policies for customer/admin access control - Indexes for query performance - Triggers for automatic timestamp updates - Complete foreign key relationships Security Features: - RLS policies: Customers can only view their own data - Admins can view and manage all data - Anonymous users can submit RFQs and cluster requests - Automatic timestamp management - Secure credential storage Form Integration: - Request Quote page now submits to Supabase rfqs table - Clusters page now submits to Supabase cluster_requests table - Both forms include proper error handling and success messages - Duration converted to months for database storage Configuration: - .env.example with Supabase URL and API key placeholders - supabase/README.md with comprehensive setup instructions - Project ID: fefkbezinxawrutwjdkq - Region: us-east-1 Helper Functions: - submitRFQ() - Submit request for quote - submitClusterRequest() - Submit cluster inquiry - getUserRFQs/Quotes/Orders/Subscriptions() - Customer data retrieval - getAllRFQs/ClusterRequests() - Admin data retrieval - updateRFQStatus/ClusterRequestStatus() - Admin workflow management Next Steps: 1. Run schema.sql in Supabase SQL Editor 2. Configure environment variables in .env.local 3. Test RFQ and cluster request submissions 4. Build customer dashboard to view submitted requests 5. Build admin dashboard to manage requests and send quotes
326 lines
13 KiB
PL/PgSQL
326 lines
13 KiB
PL/PgSQL
-- Hanzo.Computer Database Schema for Supabase
|
|
-- Run this in Supabase SQL Editor to create all tables
|
|
|
|
-- Enable UUID extension
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
-- Users table (extends Supabase auth.users)
|
|
CREATE TABLE IF NOT EXISTS public.users (
|
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
company VARCHAR(255),
|
|
stripe_customer_id VARCHAR(255),
|
|
role VARCHAR(50) DEFAULT 'customer', -- 'customer', 'admin'
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Request for Quotes (RFQs)
|
|
CREATE TABLE IF NOT EXISTS public.rfqs (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
company VARCHAR(255) NOT NULL,
|
|
email VARCHAR(255) NOT NULL,
|
|
phone VARCHAR(50),
|
|
gpu_type VARCHAR(100) NOT NULL, -- 'b300', 'b200', 'h200', 'h100', 'custom'
|
|
quantity INTEGER NOT NULL,
|
|
duration_months INTEGER, -- null for one-time purchase
|
|
use_case TEXT NOT NULL,
|
|
budget_range VARCHAR(100),
|
|
additional_requirements TEXT,
|
|
status VARCHAR(50) DEFAULT 'pending', -- 'pending', 'reviewing', 'quoted', 'accepted', 'rejected'
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Cluster Requests
|
|
CREATE TABLE IF NOT EXISTS public.cluster_requests (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
first_name VARCHAR(255) NOT NULL,
|
|
last_name VARCHAR(255) NOT NULL,
|
|
email VARCHAR(255) NOT NULL,
|
|
company VARCHAR(255) NOT NULL,
|
|
cluster_requirements TEXT NOT NULL,
|
|
number_of_gpus VARCHAR(50) NOT NULL,
|
|
rental_duration VARCHAR(50) NOT NULL,
|
|
project_description TEXT NOT NULL,
|
|
hear_about_us VARCHAR(100) NOT NULL,
|
|
status VARCHAR(50) DEFAULT 'pending', -- 'pending', 'reviewing', 'quoted', 'accepted', 'rejected'
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Quotes (sent by admin in response to RFQ or cluster request)
|
|
CREATE TABLE IF NOT EXISTS public.quotes (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
rfq_id UUID REFERENCES public.rfqs(id) ON DELETE SET NULL,
|
|
cluster_request_id UUID REFERENCES public.cluster_requests(id) ON DELETE SET NULL,
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
quote_number VARCHAR(100) UNIQUE NOT NULL, -- 'Q-2025-001'
|
|
items JSONB NOT NULL, -- [{name, description, quantity, unit_price, total}]
|
|
subtotal DECIMAL(10,2) NOT NULL,
|
|
tax DECIMAL(10,2) DEFAULT 0,
|
|
total DECIMAL(10,2) NOT NULL,
|
|
payment_terms VARCHAR(255), -- 'Monthly subscription', 'Annual prepaid', etc.
|
|
valid_until DATE,
|
|
notes TEXT,
|
|
status VARCHAR(50) DEFAULT 'sent', -- 'sent', 'viewed', 'accepted', 'expired', 'rejected'
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
accepted_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
-- Orders (from accepted quotes or direct purchases)
|
|
CREATE TABLE IF NOT EXISTS public.orders (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
order_number VARCHAR(100) UNIQUE NOT NULL, -- 'ORD-2025-001'
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
quote_id UUID REFERENCES public.quotes(id) ON DELETE SET NULL,
|
|
stripe_payment_intent_id VARCHAR(255),
|
|
stripe_subscription_id VARCHAR(255), -- for recurring billing
|
|
items JSONB NOT NULL,
|
|
subtotal DECIMAL(10,2) NOT NULL,
|
|
tax DECIMAL(10,2) DEFAULT 0,
|
|
total DECIMAL(10,2) NOT NULL,
|
|
status VARCHAR(50) DEFAULT 'pending', -- 'pending', 'paid', 'provisioning', 'active', 'cancelled'
|
|
payment_method VARCHAR(50), -- 'stripe_card', 'stripe_ach', 'wire_transfer'
|
|
billing_cycle VARCHAR(50), -- 'one_time', 'monthly', '6_month', 'annual'
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
paid_at TIMESTAMP WITH TIME ZONE,
|
|
provisioned_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
-- Subscriptions (for GPU leasing)
|
|
CREATE TABLE IF NOT EXISTS public.subscriptions (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
order_id UUID REFERENCES public.orders(id) ON DELETE SET NULL,
|
|
stripe_subscription_id VARCHAR(255) UNIQUE NOT NULL,
|
|
stripe_product_id VARCHAR(255),
|
|
stripe_price_id VARCHAR(255),
|
|
gpu_type VARCHAR(100),
|
|
quantity INTEGER,
|
|
status VARCHAR(50) DEFAULT 'active', -- 'active', 'cancelled', 'past_due', 'paused'
|
|
current_period_start TIMESTAMP WITH TIME ZONE,
|
|
current_period_end TIMESTAMP WITH TIME ZONE,
|
|
cancel_at_period_end BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
cancelled_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
-- Invoices (generated by Stripe or manually)
|
|
CREATE TABLE IF NOT EXISTS public.invoices (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
invoice_number VARCHAR(100) UNIQUE NOT NULL, -- 'INV-2025-001'
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
order_id UUID REFERENCES public.orders(id) ON DELETE SET NULL,
|
|
subscription_id UUID REFERENCES public.subscriptions(id) ON DELETE SET NULL,
|
|
stripe_invoice_id VARCHAR(255),
|
|
amount_due DECIMAL(10,2) NOT NULL,
|
|
amount_paid DECIMAL(10,2) DEFAULT 0,
|
|
status VARCHAR(50) DEFAULT 'draft', -- 'draft', 'open', 'paid', 'void', 'uncollectible'
|
|
due_date DATE,
|
|
paid_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- GPU Reservations (what customer actually gets)
|
|
CREATE TABLE IF NOT EXISTS public.gpu_reservations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
order_id UUID REFERENCES public.orders(id) ON DELETE SET NULL,
|
|
subscription_id UUID REFERENCES public.subscriptions(id) ON DELETE SET NULL,
|
|
gpu_type VARCHAR(100),
|
|
quantity INTEGER,
|
|
instance_ids JSONB, -- array of actual server/instance IDs
|
|
access_credentials JSONB, -- encrypted connection details
|
|
status VARCHAR(50) DEFAULT 'provisioning', -- 'provisioning', 'active', 'suspended', 'terminated'
|
|
provisioned_at TIMESTAMP WITH TIME ZONE,
|
|
expires_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Usage Tracking
|
|
CREATE TABLE IF NOT EXISTS public.usage_records (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
gpu_reservation_id UUID REFERENCES public.gpu_reservations(id) ON DELETE CASCADE,
|
|
hours_used DECIMAL(10,2),
|
|
compute_units DECIMAL(10,2),
|
|
recorded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Admin Actions Log
|
|
CREATE TABLE IF NOT EXISTS public.admin_actions (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
admin_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
|
action VARCHAR(100) NOT NULL, -- 'quote_sent', 'order_approved', 'subscription_cancelled', etc.
|
|
entity_type VARCHAR(50), -- 'rfq', 'quote', 'order', 'subscription'
|
|
entity_id UUID,
|
|
notes TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Create indexes for better query performance
|
|
CREATE INDEX IF NOT EXISTS idx_rfqs_user_id ON public.rfqs(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_rfqs_status ON public.rfqs(status);
|
|
CREATE INDEX IF NOT EXISTS idx_rfqs_created_at ON public.rfqs(created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_cluster_requests_user_id ON public.cluster_requests(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_cluster_requests_status ON public.cluster_requests(status);
|
|
CREATE INDEX IF NOT EXISTS idx_cluster_requests_created_at ON public.cluster_requests(created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_quotes_user_id ON public.quotes(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_quotes_rfq_id ON public.quotes(rfq_id);
|
|
CREATE INDEX IF NOT EXISTS idx_quotes_cluster_request_id ON public.quotes(cluster_request_id);
|
|
CREATE INDEX IF NOT EXISTS idx_quotes_status ON public.quotes(status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON public.orders(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_orders_status ON public.orders(status);
|
|
CREATE INDEX IF NOT EXISTS idx_orders_created_at ON public.orders(created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON public.subscriptions(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON public.subscriptions(status);
|
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_subscription_id ON public.subscriptions(stripe_subscription_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gpu_reservations_user_id ON public.gpu_reservations(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_gpu_reservations_status ON public.gpu_reservations(status);
|
|
|
|
-- Row Level Security (RLS) Policies
|
|
|
|
-- Enable RLS on all tables
|
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.rfqs ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.cluster_requests ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.quotes ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.gpu_reservations ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.usage_records ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.admin_actions ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Users: Users can read/update their own profile
|
|
CREATE POLICY "Users can view own profile" ON public.users
|
|
FOR SELECT USING (auth.uid() = id);
|
|
|
|
CREATE POLICY "Users can update own profile" ON public.users
|
|
FOR UPDATE USING (auth.uid() = id);
|
|
|
|
-- RFQs: Users can create and view their own RFQs, admins can view all
|
|
CREATE POLICY "Anyone can create RFQ" ON public.rfqs
|
|
FOR INSERT WITH CHECK (true);
|
|
|
|
CREATE POLICY "Users can view own RFQs" ON public.rfqs
|
|
FOR SELECT USING (
|
|
auth.uid() = user_id OR
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- Cluster Requests: Same as RFQs
|
|
CREATE POLICY "Anyone can create cluster request" ON public.cluster_requests
|
|
FOR INSERT WITH CHECK (true);
|
|
|
|
CREATE POLICY "Users can view own cluster requests" ON public.cluster_requests
|
|
FOR SELECT USING (
|
|
auth.uid() = user_id OR
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- Quotes: Users can view their own quotes, admins can manage all
|
|
CREATE POLICY "Users can view own quotes" ON public.quotes
|
|
FOR SELECT USING (
|
|
auth.uid() = user_id OR
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins can create quotes" ON public.quotes
|
|
FOR INSERT WITH CHECK (
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins can update quotes" ON public.quotes
|
|
FOR UPDATE USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- Orders: Users can view their own orders, admins can manage all
|
|
CREATE POLICY "Users can view own orders" ON public.orders
|
|
FOR SELECT USING (
|
|
auth.uid() = user_id OR
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Users can create orders" ON public.orders
|
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "Admins can update orders" ON public.orders
|
|
FOR UPDATE USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- Subscriptions: Users can view their own, admins can manage all
|
|
CREATE POLICY "Users can view own subscriptions" ON public.subscriptions
|
|
FOR SELECT USING (
|
|
auth.uid() = user_id OR
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins can manage subscriptions" ON public.subscriptions
|
|
FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- GPU Reservations: Users can view their own
|
|
CREATE POLICY "Users can view own GPU reservations" ON public.gpu_reservations
|
|
FOR SELECT USING (
|
|
auth.uid() = user_id OR
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins can manage GPU reservations" ON public.gpu_reservations
|
|
FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- Usage Records: Users can view their own
|
|
CREATE POLICY "Users can view own usage" ON public.usage_records
|
|
FOR SELECT USING (
|
|
auth.uid() = user_id OR
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- Admin Actions: Only admins can view
|
|
CREATE POLICY "Admins can view admin actions" ON public.admin_actions
|
|
FOR SELECT USING (
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
CREATE POLICY "Admins can create admin actions" ON public.admin_actions
|
|
FOR INSERT WITH CHECK (
|
|
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- Functions for automatic timestamp updates
|
|
CREATE OR REPLACE FUNCTION public.handle_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Triggers for updated_at
|
|
CREATE TRIGGER set_updated_at_users
|
|
BEFORE UPDATE ON public.users
|
|
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
|
|
CREATE TRIGGER set_updated_at_rfqs
|
|
BEFORE UPDATE ON public.rfqs
|
|
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
|
|
CREATE TRIGGER set_updated_at_cluster_requests
|
|
BEFORE UPDATE ON public.cluster_requests
|
|
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|