Files
tasq/supabase/migrations/20260519000000_network_infrastructure_map.sql
T
2026-05-21 06:29:14 +08:00

330 lines
14 KiB
PL/PgSQL

-- Network Infrastructure Map — V1 schema.
-- Adds 7 tables (sites, locations, devices, ports, links, vlans, imports)
-- plus a private Storage bucket for uploaded source documents.
-- RLS: read for admin/it_staff/dispatcher/programmer; write for admin/it_staff only.
-- See plan: C:\Users\marcr\.claude\plans\i-m-thinking-of-nested-lagoon.md
-- =============================================================================
-- TABLES
-- =============================================================================
-- Sites: top-level physical locations (campus, building treated standalone)
CREATE TABLE IF NOT EXISTS public.network_sites (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
address text,
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by uuid REFERENCES public.profiles(id)
);
-- Locations: nested physical hierarchy within a site
CREATE TABLE IF NOT EXISTS public.network_locations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
site_id uuid NOT NULL REFERENCES public.network_sites(id) ON DELETE CASCADE,
parent_id uuid REFERENCES public.network_locations(id) ON DELETE CASCADE,
name text NOT NULL,
kind text NOT NULL CHECK (kind IN ('building','floor','room','rack','wall_jack','other')),
position_label text,
notes text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_network_locations_site ON public.network_locations(site_id);
CREATE INDEX IF NOT EXISTS idx_network_locations_parent ON public.network_locations(parent_id);
-- Devices: switches, routers, APs, endpoints, patch panels
CREATE TABLE IF NOT EXISTS public.network_devices (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
kind text NOT NULL CHECK (kind IN
('router','switch','ap','firewall','server','endpoint','patch_panel','other')),
role text CHECK (role IN ('core','distribution','access','edge','endpoint')),
vendor text,
model text,
serial text,
mgmt_ip inet,
mac macaddr,
location_id uuid REFERENCES public.network_locations(id) ON DELETE SET NULL,
import_source text NOT NULL DEFAULT 'manual'
CHECK (import_source IN ('manual','ai_import','agent')),
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_network_devices_location ON public.network_devices(location_id);
CREATE INDEX IF NOT EXISTS idx_network_devices_role ON public.network_devices(role);
CREATE INDEX IF NOT EXISTS idx_network_devices_kind ON public.network_devices(kind);
-- Ports: physical/logical ports on a device
CREATE TABLE IF NOT EXISTS public.network_ports (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
device_id uuid NOT NULL REFERENCES public.network_devices(id) ON DELETE CASCADE,
port_number text NOT NULL,
port_kind text CHECK (port_kind IN ('rj45','sfp','sfp_plus','console','power','wireless','virtual','other')),
speed_mbps integer,
access_vlan integer,
is_trunk boolean NOT NULL DEFAULT false,
trunk_vlans integer[],
notes text,
UNIQUE (device_id, port_number)
);
CREATE INDEX IF NOT EXISTS idx_network_ports_device ON public.network_ports(device_id);
-- Links: port-to-port edges. Canonical ordering (port_a < port_b) prevents A↔B/B↔A dupes.
CREATE TABLE IF NOT EXISTS public.network_links (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
port_a uuid NOT NULL REFERENCES public.network_ports(id) ON DELETE CASCADE,
port_b uuid NOT NULL REFERENCES public.network_ports(id) ON DELETE CASCADE,
link_kind text CHECK (link_kind IN ('copper','fiber','wireless','virtual','unknown')),
cable_label text,
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (port_a < port_b),
UNIQUE (port_a, port_b)
);
-- VLANs (org-scoped, named)
CREATE TABLE IF NOT EXISTS public.network_vlans (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
vlan_id integer NOT NULL UNIQUE,
name text NOT NULL,
description text,
color text
);
-- Import sessions: each upload + extraction state + raw AI result for audit/replay
CREATE TABLE IF NOT EXISTS public.network_imports (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
storage_path text NOT NULL,
file_kind text NOT NULL
CHECK (file_kind IN ('pdf','image','vsdx','csv','xlsx','docx','other')),
status text NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','extracting','review','applied','discarded','failed')),
ai_result jsonb,
error_message text,
applied_device_ids uuid[],
applied_link_ids uuid[],
applied_at timestamptz,
created_by uuid NOT NULL REFERENCES public.profiles(id),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_network_imports_status ON public.network_imports(status);
CREATE INDEX IF NOT EXISTS idx_network_imports_created_by ON public.network_imports(created_by);
-- =============================================================================
-- updated_at TRIGGERS
-- =============================================================================
-- Reuses common pattern from prior Tasq migrations.
CREATE OR REPLACE FUNCTION public.network_map_set_updated_at()
RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_network_sites_updated_at ON public.network_sites;
CREATE TRIGGER trg_network_sites_updated_at
BEFORE UPDATE ON public.network_sites
FOR EACH ROW EXECUTE FUNCTION public.network_map_set_updated_at();
DROP TRIGGER IF EXISTS trg_network_devices_updated_at ON public.network_devices;
CREATE TRIGGER trg_network_devices_updated_at
BEFORE UPDATE ON public.network_devices
FOR EACH ROW EXECUTE FUNCTION public.network_map_set_updated_at();
-- =============================================================================
-- ROW LEVEL SECURITY
-- =============================================================================
-- Pattern matches supabase/migrations/20260223090000_services_read_only_for_standard_it_dispatcher.sql
-- READ: admin + it_staff + dispatcher + programmer
-- WRITE: admin + it_staff
ALTER TABLE public.network_sites ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.network_locations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.network_devices ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.network_ports ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.network_links ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.network_vlans ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.network_imports ENABLE ROW LEVEL SECURITY;
-- ----- network_sites -----
DROP POLICY IF EXISTS "Network sites: select" ON public.network_sites;
CREATE POLICY "Network sites: select" ON public.network_sites
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid()
AND p.role IN ('admin','it_staff','dispatcher','programmer'))
);
DROP POLICY IF EXISTS "Network sites: write" ON public.network_sites;
CREATE POLICY "Network sites: write" ON public.network_sites
FOR ALL USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
) WITH CHECK (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
-- ----- network_locations -----
DROP POLICY IF EXISTS "Network locations: select" ON public.network_locations;
CREATE POLICY "Network locations: select" ON public.network_locations
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid()
AND p.role IN ('admin','it_staff','dispatcher','programmer'))
);
DROP POLICY IF EXISTS "Network locations: write" ON public.network_locations;
CREATE POLICY "Network locations: write" ON public.network_locations
FOR ALL USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
) WITH CHECK (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
-- ----- network_devices -----
DROP POLICY IF EXISTS "Network devices: select" ON public.network_devices;
CREATE POLICY "Network devices: select" ON public.network_devices
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid()
AND p.role IN ('admin','it_staff','dispatcher','programmer'))
);
DROP POLICY IF EXISTS "Network devices: write" ON public.network_devices;
CREATE POLICY "Network devices: write" ON public.network_devices
FOR ALL USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
) WITH CHECK (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
-- ----- network_ports -----
DROP POLICY IF EXISTS "Network ports: select" ON public.network_ports;
CREATE POLICY "Network ports: select" ON public.network_ports
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid()
AND p.role IN ('admin','it_staff','dispatcher','programmer'))
);
DROP POLICY IF EXISTS "Network ports: write" ON public.network_ports;
CREATE POLICY "Network ports: write" ON public.network_ports
FOR ALL USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
) WITH CHECK (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
-- ----- network_links -----
DROP POLICY IF EXISTS "Network links: select" ON public.network_links;
CREATE POLICY "Network links: select" ON public.network_links
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid()
AND p.role IN ('admin','it_staff','dispatcher','programmer'))
);
DROP POLICY IF EXISTS "Network links: write" ON public.network_links;
CREATE POLICY "Network links: write" ON public.network_links
FOR ALL USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
) WITH CHECK (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
-- ----- network_vlans -----
DROP POLICY IF EXISTS "Network vlans: select" ON public.network_vlans;
CREATE POLICY "Network vlans: select" ON public.network_vlans
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid()
AND p.role IN ('admin','it_staff','dispatcher','programmer'))
);
DROP POLICY IF EXISTS "Network vlans: write" ON public.network_vlans;
CREATE POLICY "Network vlans: write" ON public.network_vlans
FOR ALL USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
) WITH CHECK (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
-- ----- network_imports -----
DROP POLICY IF EXISTS "Network imports: select" ON public.network_imports;
CREATE POLICY "Network imports: select" ON public.network_imports
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid()
AND p.role IN ('admin','it_staff','dispatcher','programmer'))
);
DROP POLICY IF EXISTS "Network imports: write" ON public.network_imports;
CREATE POLICY "Network imports: write" ON public.network_imports
FOR ALL USING (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
) WITH CHECK (
EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
-- =============================================================================
-- STORAGE BUCKET (private) for uploaded source documents
-- =============================================================================
-- The Edge Function `extract_topology` uses the service role to read from this
-- bucket. Authenticated admin/it_staff clients upload via signed URLs or with
-- their RLS-bound JWT.
INSERT INTO storage.buckets (id, name, public)
VALUES ('network-imports', 'network-imports', false)
ON CONFLICT (id) DO NOTHING;
-- Storage policies: only admin/it_staff can upload; same group + dispatcher/programmer can read.
DROP POLICY IF EXISTS "Network imports bucket: select" ON storage.objects;
CREATE POLICY "Network imports bucket: select" ON storage.objects
FOR SELECT USING (
bucket_id = 'network-imports'
AND EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid()
AND p.role IN ('admin','it_staff','dispatcher','programmer'))
);
DROP POLICY IF EXISTS "Network imports bucket: write" ON storage.objects;
CREATE POLICY "Network imports bucket: write" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'network-imports'
AND EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
DROP POLICY IF EXISTS "Network imports bucket: update" ON storage.objects;
CREATE POLICY "Network imports bucket: update" ON storage.objects
FOR UPDATE USING (
bucket_id = 'network-imports'
AND EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);
DROP POLICY IF EXISTS "Network imports bucket: delete" ON storage.objects;
CREATE POLICY "Network imports bucket: delete" ON storage.objects
FOR DELETE USING (
bucket_id = 'network-imports'
AND EXISTS (SELECT 1 FROM public.profiles p
WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff'))
);