Stock-Tool/supabase_schema.sql

174 lines
6.5 KiB
PL/PgSQL

-- =============================================================================
-- Ultimate Investment Tool — Supabase Schema
-- Paste this entire file into Supabase SQL Editor and click Run.
-- =============================================================================
-- -----------------------------------------------------------------------------
-- 1. TABLES
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS licenses (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
license_key TEXT UNIQUE NOT NULL,
hwid TEXT, -- NULL until first activation
tier TEXT NOT NULL DEFAULT 'monthly', -- 'weekly' | 'monthly' | 'lifetime'
expiry_date TIMESTAMPTZ, -- NULL = lifetime
active BOOLEAN NOT NULL DEFAULT true,
machines_allowed INTEGER NOT NULL DEFAULT 1,
notes TEXT, -- customer name / order ref
created_at TIMESTAMPTZ DEFAULT NOW(),
activated_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS app_versions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
version TEXT UNIQUE NOT NULL, -- e.g. '1.0.1'
gui_py_url TEXT, -- Supabase Storage URL for screener_gui.py
screener_py_url TEXT, -- Supabase Storage URL for stock_screener.py
release_notes TEXT,
is_latest BOOLEAN NOT NULL DEFAULT true,
released_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS auth_log (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
license_key TEXT,
hwid TEXT,
result TEXT, -- 'valid' | 'invalid'
reason TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Seed the initial version row so the updater has something to compare against.
INSERT INTO app_versions (version, is_latest, release_notes)
VALUES ('1.0.0', true, 'Initial release')
ON CONFLICT (version) DO NOTHING;
-- -----------------------------------------------------------------------------
-- 2. ROW LEVEL SECURITY — no direct table access for anon
-- -----------------------------------------------------------------------------
ALTER TABLE licenses ENABLE ROW LEVEL SECURITY;
ALTER TABLE app_versions ENABLE ROW LEVEL SECURITY;
ALTER TABLE auth_log ENABLE ROW LEVEL SECURITY;
-- No RLS policies = anon/authenticated cannot SELECT/INSERT/UPDATE/DELETE directly.
-- All access goes through SECURITY DEFINER functions below.
-- -----------------------------------------------------------------------------
-- 3. RPC: verify_license(p_key, p_hwid) → JSON
-- -----------------------------------------------------------------------------
-- Called by the app on every launch with the stored license key + machine HWID.
-- Handles: first-time HWID binding, expiry, disabled keys, HWID mismatch.
-- Returns: { valid, reason, tier, expiry_date }
-- -----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION verify_license(p_key TEXT, p_hwid TEXT)
RETURNS JSON
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
lic RECORD;
BEGIN
-- Look up the key
SELECT * INTO lic FROM licenses WHERE license_key = p_key;
IF NOT FOUND THEN
INSERT INTO auth_log (license_key, hwid, result, reason)
VALUES (p_key, p_hwid, 'invalid', 'key_not_found');
RETURN json_build_object(
'valid', false,
'reason', 'License key not found.'
);
END IF;
-- Check disabled
IF NOT lic.active THEN
INSERT INTO auth_log (license_key, hwid, result, reason)
VALUES (p_key, p_hwid, 'invalid', 'key_disabled');
RETURN json_build_object(
'valid', false,
'reason', 'This license has been disabled. Please contact support.'
);
END IF;
-- Check expiry (NULL = lifetime, never expires)
IF lic.expiry_date IS NOT NULL AND lic.expiry_date < NOW() THEN
INSERT INTO auth_log (license_key, hwid, result, reason)
VALUES (p_key, p_hwid, 'invalid', 'expired');
RETURN json_build_object(
'valid', false,
'reason', 'Your subscription has expired. Please renew to continue.'
);
END IF;
-- HWID check
IF lic.hwid IS NULL THEN
-- First activation — bind this machine
UPDATE licenses
SET hwid = p_hwid, activated_at = NOW()
WHERE license_key = p_key;
ELSIF lic.hwid <> p_hwid THEN
INSERT INTO auth_log (license_key, hwid, result, reason)
VALUES (p_key, p_hwid, 'invalid', 'hwid_mismatch');
RETURN json_build_object(
'valid', false,
'reason', 'This license is registered to a different machine. Contact support to transfer.'
);
END IF;
-- All checks passed
INSERT INTO auth_log (license_key, hwid, result, reason)
VALUES (p_key, p_hwid, 'valid', 'ok');
RETURN json_build_object(
'valid', true,
'reason', 'ok',
'tier', lic.tier,
'expiry_date', lic.expiry_date
);
END;
$$;
-- -----------------------------------------------------------------------------
-- 4. RPC: get_latest_version() → JSON
-- -----------------------------------------------------------------------------
-- Called by the updater after a successful license check.
-- Returns: { version, gui_py_url, screener_py_url, release_notes }
-- -----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION get_latest_version()
RETURNS JSON
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
ver RECORD;
BEGIN
SELECT * INTO ver
FROM app_versions
WHERE is_latest = true
ORDER BY released_at DESC
LIMIT 1;
IF NOT FOUND THEN
RETURN json_build_object('version', '1.0.0');
END IF;
RETURN json_build_object(
'version', ver.version,
'gui_py_url', ver.gui_py_url,
'screener_py_url', ver.screener_py_url,
'release_notes', ver.release_notes
);
END;
$$;
-- -----------------------------------------------------------------------------
-- 5. GRANTS — allow anon key to call the two RPC functions only
-- -----------------------------------------------------------------------------
GRANT EXECUTE ON FUNCTION verify_license(TEXT, TEXT) TO anon;
GRANT EXECUTE ON FUNCTION get_latest_version() TO anon;