-- Migration: 110_trusted_origins.sql -- Description: Trusted origins for API access without token -- Created: 2024-12-14 -- -- Manages which domains, IPs, and patterns can access the API without a Bearer token. -- Used by auth middleware to grant 'internal' role to trusted requests. -- ============================================================ -- TRUSTED ORIGINS TABLE -- ============================================================ CREATE TABLE IF NOT EXISTS trusted_origins ( id SERIAL PRIMARY KEY, -- Origin identification name VARCHAR(100) NOT NULL, -- Friendly name (e.g., "CannaIQ Production") origin_type VARCHAR(20) NOT NULL, -- 'domain', 'ip', or 'pattern' origin_value VARCHAR(255) NOT NULL, -- The actual value to match -- Metadata description TEXT, -- Optional notes active BOOLEAN DEFAULT TRUE, -- Tracking created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by INTEGER REFERENCES users(id), -- Constraints CONSTRAINT valid_origin_type CHECK (origin_type IN ('domain', 'ip', 'pattern')), UNIQUE(origin_type, origin_value) ); -- Index for active lookups (used by auth middleware) CREATE INDEX IF NOT EXISTS idx_trusted_origins_active ON trusted_origins(active) WHERE active = TRUE; -- Updated at trigger CREATE OR REPLACE FUNCTION update_trusted_origins_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trusted_origins_updated_at ON trusted_origins; CREATE TRIGGER trusted_origins_updated_at BEFORE UPDATE ON trusted_origins FOR EACH ROW EXECUTE FUNCTION update_trusted_origins_updated_at(); -- ============================================================ -- SEED DEFAULT TRUSTED ORIGINS -- These match the hardcoded fallbacks in middleware.ts -- ============================================================ -- Production domains INSERT INTO trusted_origins (name, origin_type, origin_value, description) VALUES ('CannaIQ Production', 'domain', 'https://cannaiq.co', 'Main CannaIQ dashboard'), ('CannaIQ Production (www)', 'domain', 'https://www.cannaiq.co', 'Main CannaIQ dashboard with www'), ('FindADispo Production', 'domain', 'https://findadispo.com', 'Consumer dispensary finder'), ('FindADispo Production (www)', 'domain', 'https://www.findadispo.com', 'Consumer dispensary finder with www'), ('Findagram Production', 'domain', 'https://findagram.co', 'Instagram-style cannabis discovery'), ('Findagram Production (www)', 'domain', 'https://www.findagram.co', 'Instagram-style cannabis discovery with www') ON CONFLICT (origin_type, origin_value) DO NOTHING; -- Wildcard patterns INSERT INTO trusted_origins (name, origin_type, origin_value, description) VALUES ('CannaBrands Subdomains', 'pattern', '^https://.*\\.cannabrands\\.app$', 'All *.cannabrands.app subdomains'), ('CannaIQ Subdomains', 'pattern', '^https://.*\\.cannaiq\\.co$', 'All *.cannaiq.co subdomains') ON CONFLICT (origin_type, origin_value) DO NOTHING; -- Local development INSERT INTO trusted_origins (name, origin_type, origin_value, description) VALUES ('Local API', 'domain', 'http://localhost:3010', 'Local backend API'), ('Local Admin', 'domain', 'http://localhost:8080', 'Local admin dashboard'), ('Local Vite Dev', 'domain', 'http://localhost:5173', 'Vite dev server') ON CONFLICT (origin_type, origin_value) DO NOTHING; -- Trusted IPs (localhost) INSERT INTO trusted_origins (name, origin_type, origin_value, description) VALUES ('Localhost IPv4', 'ip', '127.0.0.1', 'Local machine'), ('Localhost IPv6', 'ip', '::1', 'Local machine IPv6'), ('Localhost IPv6 Mapped', 'ip', '::ffff:127.0.0.1', 'IPv6-mapped IPv4 localhost') ON CONFLICT (origin_type, origin_value) DO NOTHING; -- ============================================================ -- COMMENTS -- ============================================================ COMMENT ON TABLE trusted_origins IS 'Domains, IPs, and patterns that can access API without token'; COMMENT ON COLUMN trusted_origins.origin_type IS 'domain = exact URL match, ip = IP address, pattern = regex pattern'; COMMENT ON COLUMN trusted_origins.origin_value IS 'For domain: full URL. For ip: IP address. For pattern: regex string';