Data upload
This commit is contained in:
378
sql/add_installer_json_api.sql
Normal file
378
sql/add_installer_json_api.sql
Normal 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
|
||||
-- =====================================================
|
||||
476
sql/add_installer_json_api_supabase_auth.sql
Normal file
476
sql/add_installer_json_api_supabase_auth.sql
Normal 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
444
sql/botkonzept_schema.sql
Normal 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
2
sql/init_pgvector.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
Reference in New Issue
Block a user