449 lines
20 KiB
PL/PgSQL
449 lines
20 KiB
PL/PgSQL
-- Campuses, campus-scoped access, and student campus assignment.
|
|
-- Spec sections 2 (campus structure), 3 (roles), and the campus/enrollment
|
|
-- portions of section 4.
|
|
--
|
|
-- This is the foundation the rest of the build sits on: tuition, invoicing,
|
|
-- alerts, notes and compliance are all campus-scoped, so the access helpers
|
|
-- defined here (`is_org_admin`, `has_campus_access`, `can_access_student`,
|
|
-- `can_manage_student`) are the single authority every later migration reuses.
|
|
|
|
-- ============================================================================
|
|
-- 1. ROLE HELPERS
|
|
-- ============================================================================
|
|
-- Every pre-existing policy is written as current_user_has_role('admin').
|
|
-- Rather than rewrite ~40 policies across nine migrations (each an opportunity
|
|
-- to silently widen or drop access), the seniority rule is taught to the helper
|
|
-- itself: super_admin and org_admin now satisfy 'admin' everywhere at once.
|
|
--
|
|
-- The implication is worth stating plainly: this grants the two new senior
|
|
-- roles the full reach that 'admin' already had on every existing table.
|
|
-- campus_admin deliberately does NOT satisfy 'admin' — it is campus-scoped and
|
|
-- gets its own policies below.
|
|
--
|
|
-- auth.uid() is wrapped in a scalar subquery throughout so the planner
|
|
-- evaluates it once per query rather than once per row.
|
|
|
|
CREATE OR REPLACE FUNCTION public.has_role(_user_id UUID, _role app_role)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM public.user_roles
|
|
WHERE user_id = _user_id
|
|
AND (role = _role OR (_role = 'admin' AND role IN ('org_admin', 'super_admin')))
|
|
)
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.current_user_has_role(_role app_role)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM public.user_roles
|
|
WHERE user_id = (SELECT auth.uid())
|
|
AND (role = _role OR (_role = 'admin' AND role IN ('org_admin', 'super_admin')))
|
|
)
|
|
$$;
|
|
|
|
-- Any of the listed roles. Avoids stacking OR'd calls in policy bodies.
|
|
CREATE OR REPLACE FUNCTION public.current_user_has_any_role(_roles app_role[])
|
|
RETURNS BOOLEAN
|
|
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM public.user_roles
|
|
WHERE user_id = (SELECT auth.uid()) AND role = ANY(_roles)
|
|
)
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.is_super_admin()
|
|
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$ SELECT public.current_user_has_any_role(ARRAY['super_admin']::app_role[]) $$;
|
|
|
|
-- Organization-wide administrative reach. The workhorse of most policies.
|
|
CREATE OR REPLACE FUNCTION public.is_org_admin()
|
|
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$ SELECT public.current_user_has_any_role(ARRAY['admin','org_admin','super_admin']::app_role[]) $$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.is_billing_admin()
|
|
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$ SELECT public.current_user_has_any_role(ARRAY['billing_admin','admin','org_admin','super_admin']::app_role[]) $$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.is_management()
|
|
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$ SELECT public.current_user_has_any_role(ARRAY['management','admin','org_admin','super_admin']::app_role[]) $$;
|
|
|
|
-- Read-only reviewer. Never paired with a write policy anywhere in this build.
|
|
CREATE OR REPLACE FUNCTION public.is_auditor()
|
|
RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$ SELECT public.current_user_has_any_role(ARRAY['auditor']::app_role[]) $$;
|
|
|
|
-- ============================================================================
|
|
-- 2. CAMPUSES
|
|
-- ============================================================================
|
|
-- Addresses, hours and cutoffs are ordinary editable columns, per the spec's
|
|
-- instruction to keep them configurable rather than hard-coded. The seed at the
|
|
-- bottom therefore creates the three campuses by name only and leaves the
|
|
-- address and time fields NULL for an administrator to fill in.
|
|
|
|
CREATE TABLE IF NOT EXISTS public.campuses (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL UNIQUE,
|
|
code TEXT UNIQUE,
|
|
campus_type TEXT,
|
|
|
|
physical_street1 TEXT,
|
|
physical_street2 TEXT,
|
|
physical_city TEXT,
|
|
physical_state TEXT,
|
|
physical_postal_code TEXT,
|
|
physical_country TEXT NOT NULL DEFAULT 'USA',
|
|
|
|
-- NULL mailing address means "same as physical"; the app should not require
|
|
-- duplicate entry to express the common case.
|
|
mailing_street1 TEXT,
|
|
mailing_street2 TEXT,
|
|
mailing_city TEXT,
|
|
mailing_state TEXT,
|
|
mailing_postal_code TEXT,
|
|
mailing_country TEXT,
|
|
|
|
phone TEXT,
|
|
email TEXT,
|
|
|
|
-- Per-weekday open/close, e.g. {"monday":{"open":"06:30","close":"18:00"}}.
|
|
-- JSONB because campuses keep irregular schedules and a column-per-day
|
|
-- layout would need a migration every time one changes.
|
|
operating_hours JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
early_dropoff_time TIME,
|
|
standard_start_time TIME,
|
|
standard_dismissal_time TIME,
|
|
late_pickup_cutoff TIME,
|
|
|
|
timezone TEXT NOT NULL DEFAULT 'America/New_York',
|
|
student_capacity INTEGER CHECK (student_capacity IS NULL OR student_capacity >= 0),
|
|
grade_levels_served TEXT[] NOT NULL DEFAULT '{}',
|
|
min_age_months INTEGER CHECK (min_age_months IS NULL OR min_age_months >= 0),
|
|
max_age_months INTEGER CHECK (max_age_months IS NULL OR max_age_months >= 0),
|
|
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
notes TEXT,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT campuses_age_range_ordered
|
|
CHECK (min_age_months IS NULL OR max_age_months IS NULL OR min_age_months <= max_age_months)
|
|
);
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.campuses TO authenticated;
|
|
GRANT ALL ON public.campuses TO service_role;
|
|
ALTER TABLE public.campuses ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE INDEX IF NOT EXISTS campuses_active_idx ON public.campuses (is_active) WHERE is_active;
|
|
|
|
DROP TRIGGER IF EXISTS trg_campuses_upd ON public.campuses;
|
|
CREATE TRIGGER trg_campuses_upd BEFORE UPDATE ON public.campuses
|
|
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
|
|
-- Programs offered at a campus (spec: "supported programs").
|
|
CREATE TABLE IF NOT EXISTS public.campus_programs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
campus_id UUID NOT NULL REFERENCES public.campuses(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
min_age_months INTEGER,
|
|
max_age_months INTEGER,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (campus_id, name)
|
|
);
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.campus_programs TO authenticated;
|
|
GRANT ALL ON public.campus_programs TO service_role;
|
|
ALTER TABLE public.campus_programs ENABLE ROW LEVEL SECURITY;
|
|
CREATE INDEX IF NOT EXISTS campus_programs_campus_idx ON public.campus_programs (campus_id);
|
|
|
|
-- ============================================================================
|
|
-- 3. STAFF ↔ CAMPUS ASSIGNMENT
|
|
-- ============================================================================
|
|
-- Drives campus_admin / teacher / staff scoping. A row here is what makes a
|
|
-- campus_admin an administrator *of that campus*.
|
|
|
|
CREATE TABLE IF NOT EXISTS public.staff_campus_assignments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
campus_id UUID NOT NULL REFERENCES public.campuses(id) ON DELETE CASCADE,
|
|
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
|
|
title TEXT,
|
|
start_date DATE,
|
|
end_date DATE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (user_id, campus_id),
|
|
CONSTRAINT staff_campus_dates_ordered
|
|
CHECK (start_date IS NULL OR end_date IS NULL OR start_date <= end_date)
|
|
);
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.staff_campus_assignments TO authenticated;
|
|
GRANT ALL ON public.staff_campus_assignments TO service_role;
|
|
ALTER TABLE public.staff_campus_assignments ENABLE ROW LEVEL SECURITY;
|
|
CREATE INDEX IF NOT EXISTS staff_campus_user_idx ON public.staff_campus_assignments (user_id);
|
|
CREATE INDEX IF NOT EXISTS staff_campus_campus_idx ON public.staff_campus_assignments (campus_id);
|
|
|
|
-- Campuses the caller is currently assigned to. Expired assignments (end_date
|
|
-- in the past) do not count.
|
|
CREATE OR REPLACE FUNCTION public.user_campus_ids()
|
|
RETURNS SETOF UUID
|
|
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
SELECT campus_id FROM public.staff_campus_assignments
|
|
WHERE user_id = (SELECT auth.uid())
|
|
AND (start_date IS NULL OR start_date <= CURRENT_DATE)
|
|
AND (end_date IS NULL OR end_date >= CURRENT_DATE)
|
|
$$;
|
|
|
|
-- Org admins reach every campus; everyone else only their assignments.
|
|
CREATE OR REPLACE FUNCTION public.has_campus_access(_campus UUID)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
SELECT public.is_org_admin()
|
|
OR (_campus IS NOT NULL AND _campus IN (SELECT public.user_campus_ids()))
|
|
$$;
|
|
|
|
-- ============================================================================
|
|
-- 4. STUDENT ENROLLMENT + CAMPUS FIELDS
|
|
-- ============================================================================
|
|
-- Existing first_name / last_name serve as the legal names; the spec's legal
|
|
-- middle name and preferred name are added alongside.
|
|
|
|
ALTER TABLE public.students
|
|
ADD COLUMN IF NOT EXISTS middle_name TEXT,
|
|
ADD COLUMN IF NOT EXISTS preferred_name TEXT,
|
|
ADD COLUMN IF NOT EXISTS primary_campus_id UUID REFERENCES public.campuses(id) ON DELETE SET NULL,
|
|
ADD COLUMN IF NOT EXISTS program_id UUID REFERENCES public.campus_programs(id) ON DELETE SET NULL,
|
|
ADD COLUMN IF NOT EXISTS enrollment_status TEXT NOT NULL DEFAULT 'prospective',
|
|
ADD COLUMN IF NOT EXISTS enrollment_start_date DATE,
|
|
ADD COLUMN IF NOT EXISTS enrollment_end_date DATE,
|
|
ADD COLUMN IF NOT EXISTS attendance_basis TEXT NOT NULL DEFAULT 'full_time',
|
|
ADD COLUMN IF NOT EXISTS calendar_basis TEXT NOT NULL DEFAULT 'school_year',
|
|
ADD COLUMN IF NOT EXISTS is_support_student BOOLEAN NOT NULL DEFAULT FALSE;
|
|
|
|
DO $$ BEGIN
|
|
ALTER TABLE public.students ADD CONSTRAINT students_enrollment_status_valid
|
|
CHECK (enrollment_status IN ('prospective','enrolled','waitlisted','withdrawn','graduated','on_hold'));
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
|
DO $$ BEGIN
|
|
ALTER TABLE public.students ADD CONSTRAINT students_attendance_basis_valid
|
|
CHECK (attendance_basis IN ('full_time','part_time'));
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
|
DO $$ BEGIN
|
|
ALTER TABLE public.students ADD CONSTRAINT students_calendar_basis_valid
|
|
CHECK (calendar_basis IN ('year_round','school_year'));
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
|
DO $$ BEGIN
|
|
ALTER TABLE public.students ADD CONSTRAINT students_enrollment_dates_ordered
|
|
CHECK (enrollment_start_date IS NULL OR enrollment_end_date IS NULL
|
|
OR enrollment_start_date <= enrollment_end_date);
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
|
CREATE INDEX IF NOT EXISTS students_primary_campus_idx ON public.students (primary_campus_id);
|
|
CREATE INDEX IF NOT EXISTS students_program_idx ON public.students (program_id);
|
|
CREATE INDEX IF NOT EXISTS students_enrollment_status_idx ON public.students (enrollment_status);
|
|
|
|
-- ============================================================================
|
|
-- 5. WEEKLY CAMPUS SCHEDULE
|
|
-- ============================================================================
|
|
-- One row per (student, campus, effective period). Modelling it this way is
|
|
-- what lets a student be full-time overall while part-time at two campuses in
|
|
-- the same billing week — the case the spec calls out explicitly — and the
|
|
-- effective_start/effective_end pair doubles as the student's campus history.
|
|
--
|
|
-- Seven booleans rather than a day-of-week child table: it maps directly onto
|
|
-- the checkbox grid the spec asks for, and the billing engine needs a cheap
|
|
-- per-week day count, which the generated column provides.
|
|
|
|
CREATE TABLE IF NOT EXISTS public.student_campus_schedules (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
|
campus_id UUID NOT NULL REFERENCES public.campuses(id) ON DELETE CASCADE,
|
|
assignment_type TEXT NOT NULL DEFAULT 'primary',
|
|
|
|
effective_start DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
effective_end DATE,
|
|
|
|
monday BOOLEAN NOT NULL DEFAULT FALSE,
|
|
tuesday BOOLEAN NOT NULL DEFAULT FALSE,
|
|
wednesday BOOLEAN NOT NULL DEFAULT FALSE,
|
|
thursday BOOLEAN NOT NULL DEFAULT FALSE,
|
|
friday BOOLEAN NOT NULL DEFAULT FALSE,
|
|
saturday BOOLEAN NOT NULL DEFAULT FALSE,
|
|
sunday BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
scheduled_days_per_week SMALLINT GENERATED ALWAYS AS (
|
|
(monday::int + tuesday::int + wednesday::int + thursday::int
|
|
+ friday::int + saturday::int + sunday::int)::smallint
|
|
) STORED,
|
|
|
|
early_dropoff BOOLEAN NOT NULL DEFAULT FALSE,
|
|
late_pickup BOOLEAN NOT NULL DEFAULT FALSE,
|
|
notes TEXT,
|
|
|
|
created_by UUID REFERENCES auth.users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT scs_assignment_type_valid
|
|
CHECK (assignment_type IN ('primary','additional','temporary')),
|
|
CONSTRAINT scs_dates_ordered
|
|
CHECK (effective_end IS NULL OR effective_start <= effective_end)
|
|
);
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.student_campus_schedules TO authenticated;
|
|
GRANT ALL ON public.student_campus_schedules TO service_role;
|
|
ALTER TABLE public.student_campus_schedules ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE INDEX IF NOT EXISTS scs_student_idx ON public.student_campus_schedules (student_id);
|
|
CREATE INDEX IF NOT EXISTS scs_campus_idx ON public.student_campus_schedules (campus_id);
|
|
CREATE INDEX IF NOT EXISTS scs_effective_idx
|
|
ON public.student_campus_schedules (student_id, effective_start, effective_end);
|
|
|
|
DROP TRIGGER IF EXISTS trg_scs_upd ON public.student_campus_schedules;
|
|
CREATE TRIGGER trg_scs_upd BEFORE UPDATE ON public.student_campus_schedules
|
|
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
|
|
-- Campuses a student is attached to over a date range: the primary campus plus
|
|
-- any schedule row overlapping the range. Used by access checks and billing.
|
|
CREATE OR REPLACE FUNCTION public.student_campus_ids(_student UUID, _from DATE DEFAULT NULL, _to DATE DEFAULT NULL)
|
|
RETURNS SETOF UUID
|
|
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
SELECT s.primary_campus_id
|
|
FROM public.students s
|
|
WHERE s.id = _student AND s.primary_campus_id IS NOT NULL
|
|
UNION
|
|
SELECT sc.campus_id
|
|
FROM public.student_campus_schedules sc
|
|
WHERE sc.student_id = _student
|
|
AND (_to IS NULL OR sc.effective_start <= _to)
|
|
AND (_from IS NULL OR sc.effective_end IS NULL OR sc.effective_end >= _from)
|
|
$$;
|
|
|
|
-- ============================================================================
|
|
-- 6. CENTRAL STUDENT ACCESS CHECKS
|
|
-- ============================================================================
|
|
-- Every later migration routes student-scoped RLS through these two functions,
|
|
-- so the rules live in exactly one place.
|
|
--
|
|
-- Read: org admins, campus admins/staff/teachers at any campus the student
|
|
-- attends, the student's teacher, the student's parents, and auditors.
|
|
-- Write: org admins and campus admins of the student's campuses only —
|
|
-- deliberately excluding teachers, staff, parents and auditors.
|
|
|
|
CREATE OR REPLACE FUNCTION public.can_access_student(_student UUID)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
SELECT public.is_org_admin()
|
|
OR public.is_auditor()
|
|
OR public.is_parent_of(_student)
|
|
OR public.teaches_student(_student)
|
|
OR EXISTS (
|
|
SELECT 1 FROM public.student_campus_ids(_student) c
|
|
WHERE c IN (SELECT public.user_campus_ids())
|
|
)
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.can_manage_student(_student UUID)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
|
AS $$
|
|
SELECT public.is_org_admin()
|
|
OR (
|
|
public.current_user_has_any_role(ARRAY['campus_admin']::app_role[])
|
|
AND EXISTS (
|
|
SELECT 1 FROM public.student_campus_ids(_student) c
|
|
WHERE c IN (SELECT public.user_campus_ids())
|
|
)
|
|
)
|
|
$$;
|
|
|
|
-- ============================================================================
|
|
-- 7. POLICIES
|
|
-- ============================================================================
|
|
|
|
-- Campuses are reference data: any authenticated user may read them (parents
|
|
-- need campus names and hours). Only org admins may create or delete; campus
|
|
-- admins may edit the campuses they are assigned to.
|
|
DROP POLICY IF EXISTS "campuses read" ON public.campuses;
|
|
CREATE POLICY "campuses read" ON public.campuses FOR SELECT TO authenticated USING (TRUE);
|
|
|
|
DROP POLICY IF EXISTS "campuses org admin manage" ON public.campuses;
|
|
CREATE POLICY "campuses org admin manage" ON public.campuses FOR ALL TO authenticated
|
|
USING (public.is_org_admin()) WITH CHECK (public.is_org_admin());
|
|
|
|
DROP POLICY IF EXISTS "campuses campus admin update" ON public.campuses;
|
|
CREATE POLICY "campuses campus admin update" ON public.campuses FOR UPDATE TO authenticated
|
|
USING (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[])
|
|
AND id IN (SELECT public.user_campus_ids()))
|
|
WITH CHECK (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[])
|
|
AND id IN (SELECT public.user_campus_ids()));
|
|
|
|
DROP POLICY IF EXISTS "campus_programs read" ON public.campus_programs;
|
|
CREATE POLICY "campus_programs read" ON public.campus_programs FOR SELECT TO authenticated USING (TRUE);
|
|
|
|
DROP POLICY IF EXISTS "campus_programs manage" ON public.campus_programs;
|
|
CREATE POLICY "campus_programs manage" ON public.campus_programs FOR ALL TO authenticated
|
|
USING (public.has_campus_access(campus_id)) WITH CHECK (public.has_campus_access(campus_id));
|
|
|
|
-- Staff assignments: a user always sees their own; org admins manage all;
|
|
-- campus admins manage assignments at their own campuses.
|
|
DROP POLICY IF EXISTS "staff assignments read" ON public.staff_campus_assignments;
|
|
CREATE POLICY "staff assignments read" ON public.staff_campus_assignments FOR SELECT TO authenticated
|
|
USING (user_id = (SELECT auth.uid()) OR public.has_campus_access(campus_id) OR public.is_auditor());
|
|
|
|
DROP POLICY IF EXISTS "staff assignments manage" ON public.staff_campus_assignments;
|
|
CREATE POLICY "staff assignments manage" ON public.staff_campus_assignments FOR ALL TO authenticated
|
|
USING (public.is_org_admin()
|
|
OR (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[])
|
|
AND campus_id IN (SELECT public.user_campus_ids())))
|
|
WITH CHECK (public.is_org_admin()
|
|
OR (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[])
|
|
AND campus_id IN (SELECT public.user_campus_ids())));
|
|
|
|
DROP POLICY IF EXISTS "scs read" ON public.student_campus_schedules;
|
|
CREATE POLICY "scs read" ON public.student_campus_schedules FOR SELECT TO authenticated
|
|
USING (public.can_access_student(student_id));
|
|
|
|
DROP POLICY IF EXISTS "scs manage" ON public.student_campus_schedules;
|
|
CREATE POLICY "scs manage" ON public.student_campus_schedules FOR ALL TO authenticated
|
|
USING (public.can_manage_student(student_id)) WITH CHECK (public.can_manage_student(student_id));
|
|
|
|
-- Widen the existing student policies for the campus-scoped roles. These are
|
|
-- additive: permissive policies OR together, so the original admin/parent/
|
|
-- teacher policies from the first migration keep working untouched.
|
|
DROP POLICY IF EXISTS "students campus scoped read" ON public.students;
|
|
CREATE POLICY "students campus scoped read" ON public.students FOR SELECT TO authenticated
|
|
USING (public.can_access_student(id));
|
|
|
|
DROP POLICY IF EXISTS "students campus admin manage" ON public.students;
|
|
CREATE POLICY "students campus admin manage" ON public.students FOR ALL TO authenticated
|
|
USING (public.can_manage_student(id)) WITH CHECK (public.can_manage_student(id));
|
|
|
|
-- ============================================================================
|
|
-- 8. SEED
|
|
-- ============================================================================
|
|
-- Names only. Addresses, phone numbers and bell times are left NULL on purpose:
|
|
-- the spec requires them to be administrator-configurable, and inventing
|
|
-- plausible-looking addresses would be worse than an obviously empty field.
|
|
|
|
INSERT INTO public.campuses (name, code, campus_type, is_active)
|
|
VALUES
|
|
('Meadowlane', 'MDL', 'school', TRUE),
|
|
('Enterprise', 'ENT', 'school', TRUE),
|
|
('Suntree', 'SUN', 'school', TRUE)
|
|
ON CONFLICT (name) DO NOTHING;
|