Terminal
This commit is contained in:
@@ -1,178 +0,0 @@
|
||||
-- Critical student alerts — spec section 6.
|
||||
--
|
||||
-- These are the red banners: custody limits, unauthorized pickups, severe
|
||||
-- allergies, court-ordered communication restrictions. The spec requires that
|
||||
-- every view and every acknowledgment be auditable, so both are recorded as
|
||||
-- rows rather than as a boolean on the alert.
|
||||
--
|
||||
-- Visibility is an explicit role array rather than a single sensitivity level.
|
||||
-- A severe-allergy alert must reach teachers and front-desk staff; a
|
||||
-- court-order alert usually must not. One column expresses both.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.student_alerts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
||||
|
||||
alert_type TEXT NOT NULL,
|
||||
severity TEXT NOT NULL DEFAULT 'critical',
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
effective_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
expiration_date DATE,
|
||||
|
||||
document_path TEXT,
|
||||
parenting_plan_id UUID REFERENCES public.parenting_plans(id) ON DELETE SET NULL,
|
||||
|
||||
-- Which staff roles see this alert. Org admins always see everything.
|
||||
visible_to_roles app_role[] NOT NULL
|
||||
DEFAULT ARRAY['admin','org_admin','super_admin','campus_admin','management','teacher','staff']::app_role[],
|
||||
|
||||
requires_acknowledgment BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT sa_type_valid CHECK (alert_type IN (
|
||||
'parenting_plan','custody','unauthorized_pickup','medical','allergy',
|
||||
'court_order','safety','dismissal','other')),
|
||||
CONSTRAINT sa_severity_valid CHECK (severity IN ('critical','high','info')),
|
||||
CONSTRAINT sa_dates_ordered
|
||||
CHECK (expiration_date IS NULL OR effective_date <= expiration_date)
|
||||
);
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.student_alerts TO authenticated;
|
||||
GRANT ALL ON public.student_alerts TO service_role;
|
||||
ALTER TABLE public.student_alerts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS sa_student_idx ON public.student_alerts (student_id);
|
||||
-- The hot path: "show me this student's live alerts". Partial, because expired
|
||||
-- and deactivated alerts are never on that screen.
|
||||
CREATE INDEX IF NOT EXISTS sa_active_idx ON public.student_alerts (student_id, severity)
|
||||
WHERE is_active;
|
||||
CREATE INDEX IF NOT EXISTS sa_plan_idx ON public.student_alerts (parenting_plan_id);
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_sa_upd ON public.student_alerts;
|
||||
CREATE TRIGGER trg_sa_upd BEFORE UPDATE ON public.student_alerts
|
||||
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
-- ============================================================================
|
||||
-- ACKNOWLEDGMENTS
|
||||
-- ============================================================================
|
||||
-- One row per staff member per alert. UNIQUE keeps it idempotent so a
|
||||
-- double-click cannot fabricate two acknowledgment records.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.student_alert_acknowledgments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
alert_id UUID NOT NULL REFERENCES public.student_alerts(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
acknowledged_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
note TEXT,
|
||||
UNIQUE (alert_id, user_id)
|
||||
);
|
||||
|
||||
GRANT SELECT, INSERT ON public.student_alert_acknowledgments TO authenticated;
|
||||
GRANT ALL ON public.student_alert_acknowledgments TO service_role;
|
||||
ALTER TABLE public.student_alert_acknowledgments ENABLE ROW LEVEL SECURITY;
|
||||
CREATE INDEX IF NOT EXISTS saa_alert_idx ON public.student_alert_acknowledgments (alert_id);
|
||||
CREATE INDEX IF NOT EXISTS saa_user_idx ON public.student_alert_acknowledgments (user_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- VIEW LOG
|
||||
-- ============================================================================
|
||||
-- Append-only. No UNIQUE constraint: the point is to record every occasion the
|
||||
-- alert was displayed, not merely that it was seen once. Deliberately has no
|
||||
-- UPDATE or DELETE grant for authenticated — it is evidence.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.student_alert_views (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
alert_id UUID NOT NULL REFERENCES public.student_alerts(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
viewed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
GRANT SELECT, INSERT ON public.student_alert_views TO authenticated;
|
||||
GRANT ALL ON public.student_alert_views TO service_role;
|
||||
ALTER TABLE public.student_alert_views ENABLE ROW LEVEL SECURITY;
|
||||
CREATE INDEX IF NOT EXISTS sav_alert_idx ON public.student_alert_views (alert_id, viewed_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS sav_user_idx ON public.student_alert_views (user_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- HELPERS
|
||||
-- ============================================================================
|
||||
|
||||
-- Currently-in-force alerts for a student that the caller is cleared to see.
|
||||
CREATE OR REPLACE FUNCTION public.active_student_alerts(_student UUID)
|
||||
RETURNS SETOF public.student_alerts
|
||||
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT a.* FROM public.student_alerts a
|
||||
WHERE a.student_id = _student
|
||||
AND a.is_active
|
||||
AND a.effective_date <= CURRENT_DATE
|
||||
AND (a.expiration_date IS NULL OR a.expiration_date >= CURRENT_DATE)
|
||||
AND (public.is_org_admin() OR public.current_user_has_any_role(a.visible_to_roles))
|
||||
ORDER BY CASE a.severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 ELSE 2 END,
|
||||
a.effective_date DESC
|
||||
$$;
|
||||
|
||||
-- Alerts still awaiting this user's acknowledgment.
|
||||
CREATE OR REPLACE FUNCTION public.unacknowledged_alerts(_student UUID)
|
||||
RETURNS SETOF public.student_alerts
|
||||
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT a.* FROM public.active_student_alerts(_student) a
|
||||
WHERE a.requires_acknowledgment
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.student_alert_acknowledgments k
|
||||
WHERE k.alert_id = a.id AND k.user_id = (SELECT auth.uid())
|
||||
)
|
||||
$$;
|
||||
|
||||
-- ============================================================================
|
||||
-- POLICIES
|
||||
-- ============================================================================
|
||||
|
||||
-- Staff see alerts targeted at their role; parents see their own child's
|
||||
-- alerts only when the alert is not restricted away from them.
|
||||
DROP POLICY IF EXISTS "alerts read" ON public.student_alerts;
|
||||
CREATE POLICY "alerts read" ON public.student_alerts FOR SELECT TO authenticated
|
||||
USING (
|
||||
public.is_org_admin()
|
||||
OR public.is_auditor()
|
||||
OR (public.can_access_student(student_id)
|
||||
AND public.current_user_has_any_role(visible_to_roles))
|
||||
OR (public.is_parent_of(student_id) AND 'parent'::app_role = ANY(visible_to_roles))
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "alerts manage" ON public.student_alerts;
|
||||
CREATE POLICY "alerts manage" ON public.student_alerts FOR ALL TO authenticated
|
||||
USING (public.can_manage_student(student_id))
|
||||
WITH CHECK (public.can_manage_student(student_id));
|
||||
|
||||
-- A user may only record their own acknowledgment, and never edit it after.
|
||||
DROP POLICY IF EXISTS "alert ack read" ON public.student_alert_acknowledgments;
|
||||
CREATE POLICY "alert ack read" ON public.student_alert_acknowledgments FOR SELECT TO authenticated
|
||||
USING (
|
||||
public.is_org_admin() OR public.is_auditor() OR user_id = (SELECT auth.uid())
|
||||
OR EXISTS (SELECT 1 FROM public.student_alerts a
|
||||
WHERE a.id = alert_id AND public.can_manage_student(a.student_id))
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "alert ack self insert" ON public.student_alert_acknowledgments;
|
||||
CREATE POLICY "alert ack self insert" ON public.student_alert_acknowledgments FOR INSERT TO authenticated
|
||||
WITH CHECK (user_id = (SELECT auth.uid()));
|
||||
|
||||
DROP POLICY IF EXISTS "alert views read" ON public.student_alert_views;
|
||||
CREATE POLICY "alert views read" ON public.student_alert_views FOR SELECT TO authenticated
|
||||
USING (
|
||||
public.is_org_admin() OR public.is_auditor() OR user_id = (SELECT auth.uid())
|
||||
OR EXISTS (SELECT 1 FROM public.student_alerts a
|
||||
WHERE a.id = alert_id AND public.can_manage_student(a.student_id))
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "alert views self insert" ON public.student_alert_views;
|
||||
CREATE POLICY "alert views self insert" ON public.student_alert_views FOR INSERT TO authenticated
|
||||
WITH CHECK (user_id = (SELECT auth.uid()));
|
||||
Reference in New Issue
Block a user