Data upload

This commit is contained in:
root
2026-03-07 19:33:48 +01:00
parent 4653439f05
commit 36ccdd9c04
17 changed files with 6477 additions and 0 deletions

View File

@@ -0,0 +1,378 @@
-- =====================================================
-- BotKonzept - Installer JSON API Extension
-- =====================================================
-- Extends the database schema to store and expose installer JSON data
-- safely to frontend clients (without secrets)
-- =====================================================
-- Step 1: Add installer_json column to instances table
-- =====================================================
-- Add column to store the complete installer JSON
ALTER TABLE instances
ADD COLUMN IF NOT EXISTS installer_json JSONB DEFAULT '{}'::jsonb;
-- Create index for faster JSON queries
CREATE INDEX IF NOT EXISTS idx_instances_installer_json ON instances USING gin(installer_json);
-- Add comment
COMMENT ON COLUMN instances.installer_json IS 'Complete installer JSON output from install.sh (includes secrets - use api.instance_config view for safe access)';
-- =====================================================
-- Step 2: Create safe API view (NON-SECRET data only)
-- =====================================================
-- Create API schema if it doesn't exist
CREATE SCHEMA IF NOT EXISTS api;
-- Grant usage on api schema
GRANT USAGE ON SCHEMA api TO anon, authenticated, service_role;
-- Create view that exposes only safe (non-secret) installer data
CREATE OR REPLACE VIEW api.instance_config AS
SELECT
i.id,
i.customer_id,
i.lxc_id as ctid,
i.hostname,
i.fqdn,
i.ip,
i.vlan,
i.status,
i.created_at,
-- Extract safe URLs from installer_json
jsonb_build_object(
'n8n_internal', i.installer_json->'urls'->>'n8n_internal',
'n8n_external', i.installer_json->'urls'->>'n8n_external',
'postgrest', i.installer_json->'urls'->>'postgrest',
'chat_webhook', i.installer_json->'urls'->>'chat_webhook',
'chat_internal', i.installer_json->'urls'->>'chat_internal',
'upload_form', i.installer_json->'urls'->>'upload_form',
'upload_form_internal', i.installer_json->'urls'->>'upload_form_internal'
) as urls,
-- Extract safe Supabase data (NO service_role_key, NO jwt_secret)
jsonb_build_object(
'url_external', i.installer_json->'supabase'->>'url_external',
'anon_key', i.installer_json->'supabase'->>'anon_key'
) as supabase,
-- Extract Ollama URL (safe)
jsonb_build_object(
'url', i.installer_json->'ollama'->>'url',
'model', i.installer_json->'ollama'->>'model',
'embedding_model', i.installer_json->'ollama'->>'embedding_model'
) as ollama,
-- Customer info (joined)
c.email as customer_email,
c.first_name,
c.last_name,
c.company,
c.status as customer_status
FROM instances i
JOIN customers c ON i.customer_id = c.id
WHERE i.status = 'active' AND i.deleted_at IS NULL;
-- Add comment
COMMENT ON VIEW api.instance_config IS 'Safe API view for instance configuration - exposes only non-secret data from installer JSON';
-- =====================================================
-- Step 3: Row Level Security (RLS) for API view
-- =====================================================
-- Enable RLS on the view (inherited from base table)
-- Customers can only see their own instance config
-- Policy: Allow customers to see their own instance config
CREATE POLICY instance_config_select_own ON instances
FOR SELECT
USING (
-- Allow if customer_id matches authenticated user
customer_id::text = auth.uid()::text
OR
-- Allow service_role to see all (for n8n workflows)
auth.jwt()->>'role' = 'service_role'
);
-- Grant SELECT on api.instance_config view
GRANT SELECT ON api.instance_config TO anon, authenticated, service_role;
-- =====================================================
-- Step 4: Create function to get config by customer email
-- =====================================================
-- Function to get instance config by customer email (for public access)
CREATE OR REPLACE FUNCTION api.get_instance_config_by_email(customer_email_param TEXT)
RETURNS TABLE (
id UUID,
customer_id UUID,
ctid BIGINT,
hostname VARCHAR,
fqdn VARCHAR,
ip VARCHAR,
vlan INTEGER,
status VARCHAR,
created_at TIMESTAMPTZ,
urls JSONB,
supabase JSONB,
ollama JSONB,
customer_email VARCHAR,
first_name VARCHAR,
last_name VARCHAR,
company VARCHAR,
customer_status VARCHAR
) AS $$
BEGIN
RETURN QUERY
SELECT
ic.id,
ic.customer_id,
ic.ctid,
ic.hostname,
ic.fqdn,
ic.ip,
ic.vlan,
ic.status,
ic.created_at,
ic.urls,
ic.supabase,
ic.ollama,
ic.customer_email,
ic.first_name,
ic.last_name,
ic.company,
ic.customer_status
FROM api.instance_config ic
WHERE ic.customer_email = customer_email_param
LIMIT 1;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Grant execute permission
GRANT EXECUTE ON FUNCTION api.get_instance_config_by_email(TEXT) TO anon, authenticated, service_role;
-- Add comment
COMMENT ON FUNCTION api.get_instance_config_by_email IS 'Get instance configuration by customer email - returns only non-secret data';
-- =====================================================
-- Step 5: Create function to get config by CTID
-- =====================================================
-- Function to get instance config by CTID (for internal use)
CREATE OR REPLACE FUNCTION api.get_instance_config_by_ctid(ctid_param BIGINT)
RETURNS TABLE (
id UUID,
customer_id UUID,
ctid BIGINT,
hostname VARCHAR,
fqdn VARCHAR,
ip VARCHAR,
vlan INTEGER,
status VARCHAR,
created_at TIMESTAMPTZ,
urls JSONB,
supabase JSONB,
ollama JSONB,
customer_email VARCHAR,
first_name VARCHAR,
last_name VARCHAR,
company VARCHAR,
customer_status VARCHAR
) AS $$
BEGIN
RETURN QUERY
SELECT
ic.id,
ic.customer_id,
ic.ctid,
ic.hostname,
ic.fqdn,
ic.ip,
ic.vlan,
ic.status,
ic.created_at,
ic.urls,
ic.supabase,
ic.ollama,
ic.customer_email,
ic.first_name,
ic.last_name,
ic.company,
ic.customer_status
FROM api.instance_config ic
WHERE ic.ctid = ctid_param
LIMIT 1;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Grant execute permission
GRANT EXECUTE ON FUNCTION api.get_instance_config_by_ctid(BIGINT) TO service_role;
-- Add comment
COMMENT ON FUNCTION api.get_instance_config_by_ctid IS 'Get instance configuration by CTID - for internal use only';
-- =====================================================
-- Step 6: Create public config endpoint (no auth required)
-- =====================================================
-- Function to get public config (for website registration form)
-- Returns only the registration webhook URL
CREATE OR REPLACE FUNCTION api.get_public_config()
RETURNS TABLE (
registration_webhook_url TEXT,
api_base_url TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
'https://api.botkonzept.de/webhook/botkonzept-registration'::TEXT as registration_webhook_url,
'https://api.botkonzept.de'::TEXT as api_base_url;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Grant execute permission to everyone
GRANT EXECUTE ON FUNCTION api.get_public_config() TO anon, authenticated, service_role;
-- Add comment
COMMENT ON FUNCTION api.get_public_config IS 'Get public configuration for website (registration webhook URL)';
-- =====================================================
-- Step 7: Update install.sh integration
-- =====================================================
-- This SQL will be executed after instance creation
-- The install.sh script should call this function to store the installer JSON
CREATE OR REPLACE FUNCTION api.store_installer_json(
customer_email_param TEXT,
lxc_id_param BIGINT,
installer_json_param JSONB
)
RETURNS JSONB AS $$
DECLARE
instance_record RECORD;
result JSONB;
BEGIN
-- Find the instance by customer email and lxc_id
SELECT i.id, i.customer_id INTO instance_record
FROM instances i
JOIN customers c ON i.customer_id = c.id
WHERE c.email = customer_email_param
AND i.lxc_id = lxc_id_param
LIMIT 1;
IF NOT FOUND THEN
RETURN jsonb_build_object(
'success', false,
'error', 'Instance not found for customer email and LXC ID'
);
END IF;
-- Update the installer_json column
UPDATE instances
SET installer_json = installer_json_param,
updated_at = NOW()
WHERE id = instance_record.id;
-- Return success
result := jsonb_build_object(
'success', true,
'instance_id', instance_record.id,
'customer_id', instance_record.customer_id,
'message', 'Installer JSON stored successfully'
);
RETURN result;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Grant execute permission to service_role only
GRANT EXECUTE ON FUNCTION api.store_installer_json(TEXT, BIGINT, JSONB) TO service_role;
-- Add comment
COMMENT ON FUNCTION api.store_installer_json IS 'Store installer JSON after instance creation - called by install.sh via n8n workflow';
-- =====================================================
-- Step 8: Create audit log entry for API access
-- =====================================================
-- Function to log API access
CREATE OR REPLACE FUNCTION api.log_config_access(
customer_id_param UUID,
access_type TEXT,
ip_address_param INET DEFAULT NULL
)
RETURNS VOID AS $$
BEGIN
INSERT INTO audit_log (
customer_id,
action,
entity_type,
performed_by,
ip_address,
metadata
) VALUES (
customer_id_param,
'api_config_access',
'instance_config',
'api_user',
ip_address_param,
jsonb_build_object('access_type', access_type)
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Grant execute permission
GRANT EXECUTE ON FUNCTION api.log_config_access(UUID, TEXT, INET) TO anon, authenticated, service_role;
-- =====================================================
-- Step 9: Example queries for testing
-- =====================================================
-- Example 1: Get instance config by customer email
-- SELECT * FROM api.get_instance_config_by_email('max@beispiel.de');
-- Example 2: Get instance config by CTID
-- SELECT * FROM api.get_instance_config_by_ctid(769697636);
-- Example 3: Get public config
-- SELECT * FROM api.get_public_config();
-- Example 4: Store installer JSON (called by install.sh)
-- SELECT api.store_installer_json(
-- 'max@beispiel.de',
-- 769697636,
-- '{"ctid": 769697636, "urls": {...}, ...}'::jsonb
-- );
-- =====================================================
-- Step 10: PostgREST API Routes
-- =====================================================
-- After running this SQL, the following PostgREST routes will be available:
--
-- 1. GET /api/instance_config
-- - Returns all instance configs (filtered by RLS)
-- - Requires authentication
--
-- 2. POST /rpc/get_instance_config_by_email
-- - Body: {"customer_email_param": "max@beispiel.de"}
-- - Returns instance config for specific customer
-- - No authentication required (public)
--
-- 3. POST /rpc/get_instance_config_by_ctid
-- - Body: {"ctid_param": 769697636}
-- - Returns instance config for specific CTID
-- - Requires service_role authentication
--
-- 4. POST /rpc/get_public_config
-- - Body: {}
-- - Returns public configuration (registration webhook URL)
-- - No authentication required (public)
--
-- 5. POST /rpc/store_installer_json
-- - Body: {"customer_email_param": "...", "lxc_id_param": 123, "installer_json_param": {...}}
-- - Stores installer JSON after instance creation
-- - Requires service_role authentication
-- =====================================================
-- End of API Extension
-- =====================================================

View File

@@ -0,0 +1,476 @@
-- =====================================================
-- BotKonzept - Installer JSON API (Supabase Auth)
-- =====================================================
-- Secure API using Supabase Auth JWT tokens
-- NO Service Role Key in Frontend - EVER!
-- =====================================================
-- Step 1: Add installer_json column to instances table
-- =====================================================
ALTER TABLE instances
ADD COLUMN IF NOT EXISTS installer_json JSONB DEFAULT '{}'::jsonb;
CREATE INDEX IF NOT EXISTS idx_instances_installer_json ON instances USING gin(installer_json);
COMMENT ON COLUMN instances.installer_json IS 'Complete installer JSON output from install.sh (includes secrets - use api.get_my_instance_config() for safe access)';
-- =====================================================
-- Step 2: Link instances to Supabase Auth users
-- =====================================================
-- Add owner_user_id column to link instance to Supabase Auth user
ALTER TABLE instances
ADD COLUMN IF NOT EXISTS owner_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL;
-- Create index for faster lookups
CREATE INDEX IF NOT EXISTS idx_instances_owner_user_id ON instances(owner_user_id);
COMMENT ON COLUMN instances.owner_user_id IS 'Supabase Auth user ID of the instance owner';
-- =====================================================
-- Step 3: Create safe API view (NON-SECRET data only)
-- =====================================================
CREATE SCHEMA IF NOT EXISTS api;
GRANT USAGE ON SCHEMA api TO anon, authenticated, service_role;
-- View that exposes only safe (non-secret) installer data
CREATE OR REPLACE VIEW api.instance_config AS
SELECT
i.id,
i.customer_id,
i.owner_user_id,
i.lxc_id as ctid,
i.hostname,
i.fqdn,
i.ip,
i.vlan,
i.status,
i.created_at,
-- Extract safe URLs from installer_json (NO SECRETS)
jsonb_build_object(
'n8n_internal', i.installer_json->'urls'->>'n8n_internal',
'n8n_external', i.installer_json->'urls'->>'n8n_external',
'postgrest', i.installer_json->'urls'->>'postgrest',
'chat_webhook', i.installer_json->'urls'->>'chat_webhook',
'chat_internal', i.installer_json->'urls'->>'chat_internal',
'upload_form', i.installer_json->'urls'->>'upload_form',
'upload_form_internal', i.installer_json->'urls'->>'upload_form_internal'
) as urls,
-- Extract safe Supabase data (NO service_role_key, NO jwt_secret)
jsonb_build_object(
'url_external', i.installer_json->'supabase'->>'url_external',
'anon_key', i.installer_json->'supabase'->>'anon_key'
) as supabase,
-- Extract Ollama URL (safe)
jsonb_build_object(
'url', i.installer_json->'ollama'->>'url',
'model', i.installer_json->'ollama'->>'model',
'embedding_model', i.installer_json->'ollama'->>'embedding_model'
) as ollama,
-- Customer info (joined)
c.email as customer_email,
c.first_name,
c.last_name,
c.company,
c.status as customer_status
FROM instances i
JOIN customers c ON i.customer_id = c.id
WHERE i.status = 'active' AND i.deleted_at IS NULL;
COMMENT ON VIEW api.instance_config IS 'Safe API view - exposes only non-secret data from installer JSON';
-- =====================================================
-- Step 4: Row Level Security (RLS) Policies
-- =====================================================
-- Enable RLS on instances table (if not already enabled)
ALTER TABLE instances ENABLE ROW LEVEL SECURITY;
-- Drop old policy if exists
DROP POLICY IF EXISTS instance_config_select_own ON instances;
-- Policy: Users can only see their own instances
CREATE POLICY instances_select_own ON instances
FOR SELECT
USING (
-- Allow if owner_user_id matches authenticated user
owner_user_id = auth.uid()
OR
-- Allow service_role to see all (for n8n workflows)
auth.jwt()->>'role' = 'service_role'
);
-- Grant SELECT on api.instance_config view
GRANT SELECT ON api.instance_config TO authenticated, service_role;
-- =====================================================
-- Step 5: Function to get MY instance config (Auth required)
-- =====================================================
-- Function to get instance config for authenticated user
-- Uses auth.uid() - NO email parameter (more secure)
CREATE OR REPLACE FUNCTION api.get_my_instance_config()
RETURNS TABLE (
id UUID,
customer_id UUID,
owner_user_id UUID,
ctid BIGINT,
hostname VARCHAR,
fqdn VARCHAR,
ip VARCHAR,
vlan INTEGER,
status VARCHAR,
created_at TIMESTAMPTZ,
urls JSONB,
supabase JSONB,
ollama JSONB,
customer_email VARCHAR,
first_name VARCHAR,
last_name VARCHAR,
company VARCHAR,
customer_status VARCHAR
)
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- Check if user is authenticated
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
-- Return instance config for authenticated user
RETURN QUERY
SELECT
ic.id,
ic.customer_id,
ic.owner_user_id,
ic.ctid,
ic.hostname,
ic.fqdn,
ic.ip,
ic.vlan,
ic.status,
ic.created_at,
ic.urls,
ic.supabase,
ic.ollama,
ic.customer_email,
ic.first_name,
ic.last_name,
ic.company,
ic.customer_status
FROM api.instance_config ic
WHERE ic.owner_user_id = auth.uid()
LIMIT 1;
END;
$$ LANGUAGE plpgsql;
GRANT EXECUTE ON FUNCTION api.get_my_instance_config() TO authenticated;
COMMENT ON FUNCTION api.get_my_instance_config IS 'Get instance configuration for authenticated user - uses auth.uid() for security';
-- =====================================================
-- Step 6: Function to get config by CTID (Service Role ONLY)
-- =====================================================
CREATE OR REPLACE FUNCTION api.get_instance_config_by_ctid(ctid_param BIGINT)
RETURNS TABLE (
id UUID,
customer_id UUID,
owner_user_id UUID,
ctid BIGINT,
hostname VARCHAR,
fqdn VARCHAR,
ip VARCHAR,
vlan INTEGER,
status VARCHAR,
created_at TIMESTAMPTZ,
urls JSONB,
supabase JSONB,
ollama JSONB,
customer_email VARCHAR,
first_name VARCHAR,
last_name VARCHAR,
company VARCHAR,
customer_status VARCHAR
)
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- Only service_role can call this
IF auth.jwt()->>'role' != 'service_role' THEN
RAISE EXCEPTION 'Forbidden: service_role required';
END IF;
RETURN QUERY
SELECT
ic.id,
ic.customer_id,
ic.owner_user_id,
ic.ctid,
ic.hostname,
ic.fqdn,
ic.ip,
ic.vlan,
ic.status,
ic.created_at,
ic.urls,
ic.supabase,
ic.ollama,
ic.customer_email,
ic.first_name,
ic.last_name,
ic.company,
ic.customer_status
FROM api.instance_config ic
WHERE ic.ctid = ctid_param
LIMIT 1;
END;
$$ LANGUAGE plpgsql;
GRANT EXECUTE ON FUNCTION api.get_instance_config_by_ctid(BIGINT) TO service_role;
COMMENT ON FUNCTION api.get_instance_config_by_ctid IS 'Get instance configuration by CTID - service_role only';
-- =====================================================
-- Step 7: Public config endpoint (NO auth required)
-- =====================================================
CREATE OR REPLACE FUNCTION api.get_public_config()
RETURNS TABLE (
registration_webhook_url TEXT,
api_base_url TEXT
)
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
RETURN QUERY
SELECT
'https://api.botkonzept.de/webhook/botkonzept-registration'::TEXT as registration_webhook_url,
'https://api.botkonzept.de'::TEXT as api_base_url;
END;
$$ LANGUAGE plpgsql;
GRANT EXECUTE ON FUNCTION api.get_public_config() TO anon, authenticated, service_role;
COMMENT ON FUNCTION api.get_public_config IS 'Get public configuration for website (registration webhook URL)';
-- =====================================================
-- Step 8: Store installer JSON (Service Role ONLY)
-- =====================================================
CREATE OR REPLACE FUNCTION api.store_installer_json(
customer_email_param TEXT,
lxc_id_param BIGINT,
installer_json_param JSONB
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
instance_record RECORD;
result JSONB;
BEGIN
-- Only service_role can call this
IF auth.jwt()->>'role' != 'service_role' THEN
RAISE EXCEPTION 'Forbidden: service_role required';
END IF;
-- Find the instance by customer email and lxc_id
SELECT i.id, i.customer_id, c.id as auth_user_id INTO instance_record
FROM instances i
JOIN customers c ON i.customer_id = c.id
WHERE c.email = customer_email_param
AND i.lxc_id = lxc_id_param
LIMIT 1;
IF NOT FOUND THEN
RETURN jsonb_build_object(
'success', false,
'error', 'Instance not found for customer email and LXC ID'
);
END IF;
-- Update the installer_json column
UPDATE instances
SET installer_json = installer_json_param,
updated_at = NOW()
WHERE id = instance_record.id;
-- Return success
result := jsonb_build_object(
'success', true,
'instance_id', instance_record.id,
'customer_id', instance_record.customer_id,
'message', 'Installer JSON stored successfully'
);
RETURN result;
END;
$$ LANGUAGE plpgsql;
GRANT EXECUTE ON FUNCTION api.store_installer_json(TEXT, BIGINT, JSONB) TO service_role;
COMMENT ON FUNCTION api.store_installer_json IS 'Store installer JSON after instance creation - service_role only';
-- =====================================================
-- Step 9: Link customer to Supabase Auth user
-- =====================================================
-- Function to link customer to Supabase Auth user (called during registration)
CREATE OR REPLACE FUNCTION api.link_customer_to_auth_user(
customer_email_param TEXT,
auth_user_id_param UUID
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
customer_record RECORD;
instance_record RECORD;
result JSONB;
BEGIN
-- Only service_role can call this
IF auth.jwt()->>'role' != 'service_role' THEN
RAISE EXCEPTION 'Forbidden: service_role required';
END IF;
-- Find customer by email
SELECT id INTO customer_record
FROM customers
WHERE email = customer_email_param
LIMIT 1;
IF NOT FOUND THEN
RETURN jsonb_build_object(
'success', false,
'error', 'Customer not found'
);
END IF;
-- Update all instances for this customer with owner_user_id
UPDATE instances
SET owner_user_id = auth_user_id_param,
updated_at = NOW()
WHERE customer_id = customer_record.id;
-- Return success
result := jsonb_build_object(
'success', true,
'customer_id', customer_record.id,
'auth_user_id', auth_user_id_param,
'message', 'Customer linked to auth user successfully'
);
RETURN result;
END;
$$ LANGUAGE plpgsql;
GRANT EXECUTE ON FUNCTION api.link_customer_to_auth_user(TEXT, UUID) TO service_role;
COMMENT ON FUNCTION api.link_customer_to_auth_user IS 'Link customer to Supabase Auth user - service_role only';
-- =====================================================
-- Step 10: Audit logging
-- =====================================================
CREATE OR REPLACE FUNCTION api.log_config_access(
access_type TEXT,
ip_address_param INET DEFAULT NULL
)
RETURNS VOID
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- Log access for authenticated user
IF auth.uid() IS NOT NULL THEN
INSERT INTO audit_log (
customer_id,
action,
entity_type,
performed_by,
ip_address,
metadata
)
SELECT
i.customer_id,
'api_config_access',
'instance_config',
auth.uid()::text,
ip_address_param,
jsonb_build_object('access_type', access_type)
FROM instances i
WHERE i.owner_user_id = auth.uid()
LIMIT 1;
END IF;
END;
$$ LANGUAGE plpgsql;
GRANT EXECUTE ON FUNCTION api.log_config_access(TEXT, INET) TO authenticated, service_role;
-- =====================================================
-- Step 11: PostgREST API Routes
-- =====================================================
-- Available routes:
--
-- 1. POST /rpc/get_my_instance_config
-- - Body: {}
-- - Returns instance config for authenticated user
-- - Requires: Supabase Auth JWT token
-- - Response: Single instance config object (or empty if not found)
--
-- 2. POST /rpc/get_public_config
-- - Body: {}
-- - Returns public configuration (registration webhook URL)
-- - Requires: No authentication
--
-- 3. POST /rpc/get_instance_config_by_ctid
-- - Body: {"ctid_param": 769697636}
-- - Returns instance config for specific CTID
-- - Requires: Service Role Key (backend only)
--
-- 4. POST /rpc/store_installer_json
-- - Body: {"customer_email_param": "...", "lxc_id_param": 123, "installer_json_param": {...}}
-- - Stores installer JSON after instance creation
-- - Requires: Service Role Key (backend only)
--
-- 5. POST /rpc/link_customer_to_auth_user
-- - Body: {"customer_email_param": "...", "auth_user_id_param": "..."}
-- - Links customer to Supabase Auth user
-- - Requires: Service Role Key (backend only)
-- =====================================================
-- Example Usage
-- =====================================================
-- Example 1: Get my instance config (authenticated user)
-- POST /rpc/get_my_instance_config
-- Headers: Authorization: Bearer <USER_JWT_TOKEN>
-- Body: {}
-- Example 2: Get public config (no auth)
-- POST /rpc/get_public_config
-- Body: {}
-- Example 3: Store installer JSON (service role)
-- POST /rpc/store_installer_json
-- Headers: Authorization: Bearer <SERVICE_ROLE_KEY>
-- Body: {"customer_email_param": "max@beispiel.de", "lxc_id_param": 769697636, "installer_json_param": {...}}
-- Example 4: Link customer to auth user (service role)
-- POST /rpc/link_customer_to_auth_user
-- Headers: Authorization: Bearer <SERVICE_ROLE_KEY>
-- Body: {"customer_email_param": "max@beispiel.de", "auth_user_id_param": "550e8400-e29b-41d4-a716-446655440000"}
-- =====================================================
-- End of Supabase Auth API
-- =====================================================

444
sql/botkonzept_schema.sql Normal file
View File

@@ -0,0 +1,444 @@
-- =====================================================
-- BotKonzept - Database Schema for Customer Management
-- =====================================================
-- This schema manages customers, instances, emails, and payments
-- for the BotKonzept SaaS platform
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- =====================================================
-- Table: customers
-- =====================================================
-- Stores customer information and trial status
CREATE TABLE IF NOT EXISTS customers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) UNIQUE NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
company VARCHAR(255),
phone VARCHAR(50),
-- Status tracking
status VARCHAR(50) DEFAULT 'trial' CHECK (status IN ('trial', 'active', 'cancelled', 'suspended', 'deleted')),
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
trial_end_date TIMESTAMPTZ,
subscription_start_date TIMESTAMPTZ,
subscription_end_date TIMESTAMPTZ,
-- Marketing tracking
utm_source VARCHAR(100),
utm_medium VARCHAR(100),
utm_campaign VARCHAR(100),
referral_code VARCHAR(50),
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb,
-- Indexes
CONSTRAINT email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);
-- Create indexes for customers
CREATE INDEX idx_customers_email ON customers(email);
CREATE INDEX idx_customers_status ON customers(status);
CREATE INDEX idx_customers_created_at ON customers(created_at);
CREATE INDEX idx_customers_trial_end_date ON customers(trial_end_date);
-- =====================================================
-- Table: instances
-- =====================================================
-- Stores LXC instance information for each customer
CREATE TABLE IF NOT EXISTS instances (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
-- Instance details
lxc_id BIGINT NOT NULL UNIQUE,
hostname VARCHAR(255) NOT NULL,
ip VARCHAR(50) NOT NULL,
fqdn VARCHAR(255) NOT NULL,
vlan INTEGER,
-- Status
status VARCHAR(50) DEFAULT 'active' CHECK (status IN ('creating', 'active', 'suspended', 'deleted', 'error')),
-- Credentials (encrypted JSON)
credentials JSONB NOT NULL,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
trial_end_date TIMESTAMPTZ,
-- Resource usage
disk_usage_gb DECIMAL(10,2),
memory_usage_mb INTEGER,
cpu_usage_percent DECIMAL(5,2),
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb
);
-- Create indexes for instances
CREATE INDEX idx_instances_customer_id ON instances(customer_id);
CREATE INDEX idx_instances_lxc_id ON instances(lxc_id);
CREATE INDEX idx_instances_status ON instances(status);
CREATE INDEX idx_instances_hostname ON instances(hostname);
-- =====================================================
-- Table: emails_sent
-- =====================================================
-- Tracks all emails sent to customers
CREATE TABLE IF NOT EXISTS emails_sent (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
-- Email details
email_type VARCHAR(50) NOT NULL CHECK (email_type IN (
'welcome',
'day3_upgrade',
'day5_reminder',
'day7_last_chance',
'day8_goodbye',
'payment_confirm',
'payment_failed',
'instance_created',
'instance_deleted',
'password_reset',
'newsletter'
)),
subject VARCHAR(255),
recipient_email VARCHAR(255) NOT NULL,
-- Status
status VARCHAR(50) DEFAULT 'sent' CHECK (status IN ('sent', 'delivered', 'opened', 'clicked', 'bounced', 'failed')),
-- Timestamps
sent_at TIMESTAMPTZ DEFAULT NOW(),
delivered_at TIMESTAMPTZ,
opened_at TIMESTAMPTZ,
clicked_at TIMESTAMPTZ,
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb
);
-- Create indexes for emails_sent
CREATE INDEX idx_emails_customer_id ON emails_sent(customer_id);
CREATE INDEX idx_emails_type ON emails_sent(email_type);
CREATE INDEX idx_emails_sent_at ON emails_sent(sent_at);
CREATE INDEX idx_emails_status ON emails_sent(status);
-- =====================================================
-- Table: subscriptions
-- =====================================================
-- Stores subscription and payment information
CREATE TABLE IF NOT EXISTS subscriptions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
-- Plan details
plan_name VARCHAR(50) NOT NULL CHECK (plan_name IN ('trial', 'starter', 'business', 'enterprise')),
plan_price DECIMAL(10,2) NOT NULL,
billing_cycle VARCHAR(20) DEFAULT 'monthly' CHECK (billing_cycle IN ('monthly', 'yearly')),
-- Discount
discount_percent DECIMAL(5,2) DEFAULT 0,
discount_code VARCHAR(50),
discount_end_date TIMESTAMPTZ,
-- Status
status VARCHAR(50) DEFAULT 'active' CHECK (status IN ('active', 'cancelled', 'past_due', 'suspended')),
-- Payment provider
payment_provider VARCHAR(50) CHECK (payment_provider IN ('stripe', 'paypal', 'manual')),
payment_provider_id VARCHAR(255),
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
current_period_start TIMESTAMPTZ,
current_period_end TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb
);
-- Create indexes for subscriptions
CREATE INDEX idx_subscriptions_customer_id ON subscriptions(customer_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE INDEX idx_subscriptions_plan_name ON subscriptions(plan_name);
-- =====================================================
-- Table: payments
-- =====================================================
-- Stores payment transaction history
CREATE TABLE IF NOT EXISTS payments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
subscription_id UUID REFERENCES subscriptions(id) ON DELETE SET NULL,
-- Payment details
amount DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'EUR',
-- Status
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'succeeded', 'failed', 'refunded', 'cancelled')),
-- Payment provider
payment_provider VARCHAR(50) CHECK (payment_provider IN ('stripe', 'paypal', 'manual')),
payment_provider_id VARCHAR(255),
payment_method VARCHAR(50),
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
paid_at TIMESTAMPTZ,
refunded_at TIMESTAMPTZ,
-- Invoice
invoice_number VARCHAR(50),
invoice_url TEXT,
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb
);
-- Create indexes for payments
CREATE INDEX idx_payments_customer_id ON payments(customer_id);
CREATE INDEX idx_payments_subscription_id ON payments(subscription_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_created_at ON payments(created_at);
-- =====================================================
-- Table: usage_stats
-- =====================================================
-- Tracks usage statistics for each instance
CREATE TABLE IF NOT EXISTS usage_stats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
instance_id UUID NOT NULL REFERENCES instances(id) ON DELETE CASCADE,
-- Usage metrics
date DATE NOT NULL,
messages_count INTEGER DEFAULT 0,
documents_count INTEGER DEFAULT 0,
api_calls_count INTEGER DEFAULT 0,
storage_used_mb DECIMAL(10,2) DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
-- Unique constraint: one record per instance per day
UNIQUE(instance_id, date)
);
-- Create indexes for usage_stats
CREATE INDEX idx_usage_instance_id ON usage_stats(instance_id);
CREATE INDEX idx_usage_date ON usage_stats(date);
-- =====================================================
-- Table: audit_log
-- =====================================================
-- Audit trail for important actions
CREATE TABLE IF NOT EXISTS audit_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
customer_id UUID REFERENCES customers(id) ON DELETE SET NULL,
instance_id UUID REFERENCES instances(id) ON DELETE SET NULL,
-- Action details
action VARCHAR(100) NOT NULL,
entity_type VARCHAR(50),
entity_id UUID,
-- User/system that performed the action
performed_by VARCHAR(100),
ip_address INET,
user_agent TEXT,
-- Changes
old_values JSONB,
new_values JSONB,
-- Timestamp
created_at TIMESTAMPTZ DEFAULT NOW(),
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb
);
-- Create indexes for audit_log
CREATE INDEX idx_audit_customer_id ON audit_log(customer_id);
CREATE INDEX idx_audit_instance_id ON audit_log(instance_id);
CREATE INDEX idx_audit_action ON audit_log(action);
CREATE INDEX idx_audit_created_at ON audit_log(created_at);
-- =====================================================
-- Functions & Triggers
-- =====================================================
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Triggers for updated_at
CREATE TRIGGER update_customers_updated_at BEFORE UPDATE ON customers
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_instances_updated_at BEFORE UPDATE ON instances
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_subscriptions_updated_at BEFORE UPDATE ON subscriptions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Function to calculate trial end date
CREATE OR REPLACE FUNCTION set_trial_end_date()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.trial_end_date IS NULL THEN
NEW.trial_end_date = NEW.created_at + INTERVAL '7 days';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger for trial end date
CREATE TRIGGER set_customer_trial_end_date BEFORE INSERT ON customers
FOR EACH ROW EXECUTE FUNCTION set_trial_end_date();
-- =====================================================
-- Views
-- =====================================================
-- View: Active trials expiring soon
CREATE OR REPLACE VIEW trials_expiring_soon AS
SELECT
c.id,
c.email,
c.first_name,
c.last_name,
c.created_at,
c.trial_end_date,
EXTRACT(DAY FROM (c.trial_end_date - NOW())) as days_remaining,
i.lxc_id,
i.hostname,
i.fqdn
FROM customers c
JOIN instances i ON c.id = i.customer_id
WHERE c.status = 'trial'
AND i.status = 'active'
AND c.trial_end_date > NOW()
AND c.trial_end_date <= NOW() + INTERVAL '3 days';
-- View: Customer overview with instance info
CREATE OR REPLACE VIEW customer_overview AS
SELECT
c.id,
c.email,
c.first_name,
c.last_name,
c.company,
c.status,
c.created_at,
c.trial_end_date,
i.lxc_id,
i.hostname,
i.fqdn,
i.ip,
i.status as instance_status,
s.plan_name,
s.plan_price,
s.status as subscription_status
FROM customers c
LEFT JOIN instances i ON c.id = i.customer_id AND i.status = 'active'
LEFT JOIN subscriptions s ON c.id = s.customer_id AND s.status = 'active';
-- View: Revenue metrics
CREATE OR REPLACE VIEW revenue_metrics AS
SELECT
DATE_TRUNC('month', paid_at) as month,
COUNT(*) as payment_count,
SUM(amount) as total_revenue,
AVG(amount) as average_payment,
COUNT(DISTINCT customer_id) as unique_customers
FROM payments
WHERE status = 'succeeded'
AND paid_at IS NOT NULL
GROUP BY DATE_TRUNC('month', paid_at)
ORDER BY month DESC;
-- =====================================================
-- Row Level Security (RLS) Policies
-- =====================================================
-- Enable RLS on tables
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
ALTER TABLE instances ENABLE ROW LEVEL SECURITY;
ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
ALTER TABLE payments ENABLE ROW LEVEL SECURITY;
-- Policy: Customers can only see their own data
CREATE POLICY customers_select_own ON customers
FOR SELECT
USING (auth.uid()::text = id::text);
CREATE POLICY instances_select_own ON instances
FOR SELECT
USING (customer_id::text = auth.uid()::text);
CREATE POLICY subscriptions_select_own ON subscriptions
FOR SELECT
USING (customer_id::text = auth.uid()::text);
CREATE POLICY payments_select_own ON payments
FOR SELECT
USING (customer_id::text = auth.uid()::text);
-- =====================================================
-- Sample Data (for testing)
-- =====================================================
-- Insert sample customer (commented out for production)
-- INSERT INTO customers (email, first_name, last_name, company, status)
-- VALUES ('test@example.com', 'Max', 'Mustermann', 'Test GmbH', 'trial');
-- =====================================================
-- Grants
-- =====================================================
-- Grant permissions to authenticated users
GRANT SELECT, INSERT, UPDATE ON customers TO authenticated;
GRANT SELECT ON instances TO authenticated;
GRANT SELECT ON subscriptions TO authenticated;
GRANT SELECT ON payments TO authenticated;
GRANT SELECT ON usage_stats TO authenticated;
-- Grant all permissions to service role (for n8n workflows)
GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO service_role;
-- =====================================================
-- Comments
-- =====================================================
COMMENT ON TABLE customers IS 'Stores customer information and trial status';
COMMENT ON TABLE instances IS 'Stores LXC instance information for each customer';
COMMENT ON TABLE emails_sent IS 'Tracks all emails sent to customers';
COMMENT ON TABLE subscriptions IS 'Stores subscription and payment information';
COMMENT ON TABLE payments IS 'Stores payment transaction history';
COMMENT ON TABLE usage_stats IS 'Tracks usage statistics for each instance';
COMMENT ON TABLE audit_log IS 'Audit trail for important actions';
-- =====================================================
-- End of Schema
-- =====================================================

2
sql/init_pgvector.sql Normal file
View File

@@ -0,0 +1,2 @@
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";