feat(equipment): schema + RLS + permission (data layer)

Migration 033 adds equipment (registry; status, optional
required_certification_id, is_active archive) and equipment_reservations
(member time-window reservation; denormalized space_id). RLS is additive
and default-deny: equipment readable by managers (all) or members
(active only), reservations by managers (all) or the member (own) with
manager-only UPDATE and no INSERT/DELETE policy so reserve/cancel
funnels through a validated service-client action (status, no-overlap
and required-certification enforced there, with manager override). New
equipment.manage permission (group Equipment) seeded to board and
backfilled. Mirrored idempotently in schema.sql; types and schema docs
updated.
This commit is contained in:
Moheeb Zara
2026-05-16 19:48:43 -07:00
parent 209622867f
commit 38fdfb0cd1
6 changed files with 264 additions and 2 deletions
+4 -1
View File
@@ -398,10 +398,11 @@ Default channels created by trigger on space INSERT: `general`, `announcements`,
| 030 | `certifications`, `member_certifications` + `certifications.manage` / `certifications.grant` permissions (the latter = Instructor). Additive default-deny RLS; grants soft-revoked + immutable (no DELETE policy); expiry snapshotted at grant; no anonymous path | | 030 | `certifications`, `member_certifications` + `certifications.manage` / `certifications.grant` permissions (the latter = Instructor). Additive default-deny RLS; grants soft-revoked + immutable (no DELETE policy); expiry snapshotted at grant; no anonymous path |
| 031 | `secrets_select` additively also honors `ops.secrets.read` (previously admin/board OR per-secret `ops_acl` only). Reveal/list gates let RLS decide; writes unchanged. Access-neutral unless the permission is granted | | 031 | `secrets_select` additively also honors `ops.secrets.read` (previously admin/board OR per-secret `ops_acl` only). Reveal/list gates let RLS decide; writes unchanged. Access-neutral unless the permission is granted |
| 032 | `classes`, `class_sessions`, `class_signups` + `classes.manage` / `classes.instruct` permissions. Additive default-deny RLS; signup/cancel via validated service-client action (no INSERT/DELETE policy); one non-cancelled signup per member+session; optional cert-on-completion via the normal certifications path | | 032 | `classes`, `class_sessions`, `class_signups` + `classes.manage` / `classes.instruct` permissions. Additive default-deny RLS; signup/cancel via validated service-client action (no INSERT/DELETE policy); one non-cancelled signup per member+session; optional cert-on-completion via the normal certifications path |
| 033 | `equipment`, `equipment_reservations` + `equipment.manage` permission. Additive default-deny RLS; reserve/cancel via validated service-client action (no INSERT/DELETE policy) enforcing status + no-overlap + required-cert with manager override |
--- ---
## Tables added by migrations 016-032 (quick map) ## Tables added by migrations 016-033 (quick map)
| Table | Key columns | Purpose | | Table | Key columns | Purpose |
|-------|-------------|---------| |-------|-------------|---------|
@@ -425,6 +426,8 @@ Default channels created by trigger on space INSERT: `general`, `announcements`,
| `classes` | space_id, title, description, payment_link (http(s) only), capacity, is_active, grants_certification_id -> certifications | Class offering. SELECT = `classes.manage` (all) or member (`is_active`); writes = `classes.manage` | | `classes` | space_id, title, description, payment_link (http(s) only), capacity, is_active, grants_certification_id -> certifications | Class offering. SELECT = `classes.manage` (all) or member (`is_active`); writes = `classes.manage` |
| `class_sessions` | class_id, space_id, starts_at, ends_at, location, capacity, status (scheduled/cancelled/completed), notes | Scheduled occurrence. SELECT = any space member; writes = `classes.manage` | | `class_sessions` | class_id, space_id, starts_at, ends_at, location, capacity, status (scheduled/cancelled/completed), notes | Scheduled occurrence. SELECT = any space member; writes = `classes.manage` |
| `class_signups` | session_id, space_id, member_id, status (registered/waitlisted/cancelled), attended, signed_up_at | Member signup. Partial UNIQUE = one non-cancelled signup per member+session. SELECT = manage/instruct (all) or member (own); UPDATE = `classes.instruct`; NO INSERT/DELETE policy (signup/cancel via validated service-client action) | | `class_signups` | session_id, space_id, member_id, status (registered/waitlisted/cancelled), attended, signed_up_at | Member signup. Partial UNIQUE = one non-cancelled signup per member+session. SELECT = manage/instruct (all) or member (own); UPDATE = `classes.instruct`; NO INSERT/DELETE policy (signup/cancel via validated service-client action) |
| `equipment` | space_id, name, description, location, status (available/maintenance/retired), required_certification_id -> certifications, asset_tag, is_active | Tool/equipment registry. SELECT = `equipment.manage` (all) or member (`is_active`); writes = `equipment.manage` |
| `equipment_reservations` | equipment_id, space_id, member_id, starts_at, ends_at, status (reserved/cancelled/completed), notes | Time-window reservation. SELECT = `equipment.manage` (all) or member (own); UPDATE = `equipment.manage`; NO INSERT/DELETE policy (reserve/cancel via validated service-client action; no-overlap + required-cert enforced there) |
Column additions: `space_members.tier_id`, `onboarding_completed_at`, `onboarding_progress`; `secrets.encrypted_value`, `encryption_version`; `knowledge_base.render_markdown`, `is_meeting_minutes`, `meeting_date`. New enum `comment_entity_type`. Column additions: `space_members.tier_id`, `onboarding_completed_at`, `onboarding_progress`; `secrets.encrypted_value`, `encryption_version`; `knowledge_base.render_markdown`, `is_meeting_minutes`, `meeting_date`. New enum `comment_entity_type`.
+2 -1
View File
@@ -686,7 +686,7 @@ CREATE INDEX idx_activity_space ON activity_log(space_id, created_at);
--- ---
## Migrations 014-032 (additions since the 13-table baseline) ## Migrations 014-033 (additions since the 13-table baseline)
`scripts/schema.sql` is the canonical idempotent schema; each numbered `scripts/schema.sql` is the canonical idempotent schema; each numbered
migration is mirrored as a section in it. Tables/columns added: migration is mirrored as a section in it. Tables/columns added:
@@ -712,6 +712,7 @@ migration is mirrored as a section in it. Tables/columns added:
| 030 | `certifications` (per-space cert types; optional `validity_months`; `is_active` archive) + `member_certifications` (per-member grants; `expires_at` snapshotted at grant time; soft `revoked_at`/`revoked_by`; partial unique on one active grant per member+cert). RLS additive + default-deny: certifications SELECT = any space member, write = `certifications.manage`; member_certifications SELECT = `certifications.manage`/`certifications.grant` (all) or member (own), INSERT/UPDATE = `certifications.grant`, NO DELETE policy (history immutable). New `certifications.manage` + `certifications.grant` permissions (the latter = the Instructor capability) seeded to board + backfilled. No anonymous path | | 030 | `certifications` (per-space cert types; optional `validity_months`; `is_active` archive) + `member_certifications` (per-member grants; `expires_at` snapshotted at grant time; soft `revoked_at`/`revoked_by`; partial unique on one active grant per member+cert). RLS additive + default-deny: certifications SELECT = any space member, write = `certifications.manage`; member_certifications SELECT = `certifications.manage`/`certifications.grant` (all) or member (own), INSERT/UPDATE = `certifications.grant`, NO DELETE policy (history immutable). New `certifications.manage` + `certifications.grant` permissions (the latter = the Instructor capability) seeded to board + backfilled. No anonymous path |
| 031 | `secrets_select` additively also honors the `ops.secrets.read` role permission (was admin/board OR per-secret `ops_acl` only; the permission was never consulted). Access-neutral unless an admin has granted the permission. Reveal/list server-side gates updated to let RLS be the boundary; write paths unchanged | | 031 | `secrets_select` additively also honors the `ops.secrets.read` role permission (was admin/board OR per-secret `ops_acl` only; the permission was never consulted). Access-neutral unless an admin has granted the permission. Reveal/list server-side gates updated to let RLS be the boundary; write paths unchanged |
| 032 | `classes`, `class_sessions`, `class_signups` (class offerings, scheduled sessions, member signups; optional `payment_link` and `grants_certification_id`; partial unique = one non-cancelled signup per member+session). RLS additive + default-deny: classes SELECT = `classes.manage` (all) or member (`is_active`), class_sessions SELECT = any space member, class_signups SELECT = manage/instruct (all) or member (own), UPDATE = `classes.instruct`, NO INSERT/DELETE policy (signup/cancel via a validated service-client action enforcing capacity/waitlist/dedupe). New `classes.manage` + `classes.instruct` permissions seeded to board + backfilled. Cert-on-completion uses the normal certifications path (still needs `certifications.grant`). No anonymous path | | 032 | `classes`, `class_sessions`, `class_signups` (class offerings, scheduled sessions, member signups; optional `payment_link` and `grants_certification_id`; partial unique = one non-cancelled signup per member+session). RLS additive + default-deny: classes SELECT = `classes.manage` (all) or member (`is_active`), class_sessions SELECT = any space member, class_signups SELECT = manage/instruct (all) or member (own), UPDATE = `classes.instruct`, NO INSERT/DELETE policy (signup/cancel via a validated service-client action enforcing capacity/waitlist/dedupe). New `classes.manage` + `classes.instruct` permissions seeded to board + backfilled. Cert-on-completion uses the normal certifications path (still needs `certifications.grant`). No anonymous path |
| 033 | `equipment` (registry; status available/maintenance/retired; optional `required_certification_id`; `is_active` archive) + `equipment_reservations` (member time-window reservation; denormalized space_id; no DB overlap constraint). RLS additive + default-deny: equipment SELECT = `equipment.manage` (all) or member (`is_active`), reservations SELECT = `equipment.manage` (all) or member (own), UPDATE = `equipment.manage`, NO INSERT/DELETE policy (reserve/cancel via validated service-client action enforcing status + no-overlap + required-cert, manager override). New `equipment.manage` permission seeded to board + backfilled. No anonymous path |
New enum: `comment_entity_type` = `forum_thread | proposal | incident | policy`. New enum: `comment_entity_type` = `forum_thread | proposal | incident | policy`.
+2
View File
@@ -26,6 +26,7 @@ export const PERMISSIONS = [
{ code: 'certifications.grant', group: 'Certifications', label: 'Award and revoke certifications (Instructor)' }, { code: 'certifications.grant', group: 'Certifications', label: 'Award and revoke certifications (Instructor)' },
{ code: 'classes.manage', group: 'Classes', label: 'Manage classes and schedule sessions' }, { code: 'classes.manage', group: 'Classes', label: 'Manage classes and schedule sessions' },
{ code: 'classes.instruct', group: 'Classes', label: 'Run classes: attendance, completion, attendees' }, { code: 'classes.instruct', group: 'Classes', label: 'Run classes: attendance, completion, attendees' },
{ code: 'equipment.manage', group: 'Equipment', label: 'Manage equipment and reservations' },
{ code: 'customize.manage', group: 'Admin', label: 'Customize roles, tiers, areas, invites, onboarding' }, { code: 'customize.manage', group: 'Admin', label: 'Customize roles, tiers, areas, invites, onboarding' },
{ code: 'settings.manage', group: 'Admin', label: 'Space settings, integrations, webhooks' }, { code: 'settings.manage', group: 'Admin', label: 'Space settings, integrations, webhooks' },
] as const ] as const
@@ -47,6 +48,7 @@ export const DEFAULT_ROLE_PERMISSIONS: Record<string, PermissionCode[]> = {
'forum.moderate', 'forms.manage', 'forum.moderate', 'forms.manage',
'certifications.manage', 'certifications.grant', 'certifications.manage', 'certifications.grant',
'classes.manage', 'classes.instruct', 'classes.manage', 'classes.instruct',
'equipment.manage',
'customize.manage', 'settings.manage', 'customize.manage', 'settings.manage',
], ],
treasurer: ['payments.manage', 'ops.kb.read', 'ops.process.read'], treasurer: ['payments.manage', 'ops.kb.read', 'ops.process.read'],
+146
View File
@@ -0,0 +1,146 @@
-- =============================================================================
-- 033: Equipment registry + reservations
-- =============================================================================
-- Two tables:
--
-- equipment a per-space tool/equipment record: name,
-- description, location, operational status
-- (available/maintenance/retired), optional
-- required_certification_id (reserving a cert-gated
-- tool requires the member hold that certification),
-- asset_tag, is_active archive flag.
-- equipment_reservations a member's time-window reservation of one piece of
-- equipment (reserved/cancelled/completed).
-- space_id is denormalized for RLS without a join.
-- Active reservations for the same equipment may not
-- overlap (enforced in the validated action).
--
-- One new permission code (lib/permissions-catalog.ts, group "Equipment"):
--
-- equipment.manage registry CRUD, status/maintenance, and adjusting or
-- cancelling any reservation (the override).
--
-- Reserving needs no permission, only space membership: it is gated by the
-- equipment being available and, when set, by the member holding the
-- required certification (manager override allowed). The cert check uses the
-- normal certifications data; no service-role bypass of that surface.
--
-- RLS posture (additive, default-deny; the guarded surface):
--
-- * equipment SELECT: equipment.manage holders see all; ordinary members
-- see only is_active (mirrors classes_select). Writes: equipment.manage.
-- * equipment_reservations SELECT: equipment.manage (all in space) or the
-- member (own). UPDATE: equipment.manage. NO INSERT/DELETE policy:
-- reserve/cancel funnels through one validated service-client action
-- that enforces status, the no-overlap rule and the required-cert gate
-- (same pattern as class_signups / form_submissions). No anonymous path.
--
-- Extends seed_default_role_permissions() so new spaces seed the code to
-- board, and backfills board -> equipment.manage for existing spaces.
--
-- Idempotent: CREATE TABLE/INDEX IF NOT EXISTS, DROP POLICY IF EXISTS then
-- CREATE, CREATE OR REPLACE FUNCTION, ON CONFLICT DO NOTHING backfill.
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.equipment (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 200),
description text,
location text,
status text NOT NULL DEFAULT 'available'
CHECK (status IN ('available','maintenance','retired')),
required_certification_id uuid REFERENCES public.certifications(id) ON DELETE SET NULL,
asset_tag text,
is_active boolean NOT NULL DEFAULT true,
created_by uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_equipment_space ON public.equipment (space_id, is_active);
CREATE TABLE IF NOT EXISTS public.equipment_reservations (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
equipment_id uuid NOT NULL REFERENCES public.equipment(id) ON DELETE CASCADE,
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
member_id uuid NOT NULL REFERENCES public.space_members(id) ON DELETE CASCADE,
starts_at timestamptz NOT NULL,
ends_at timestamptz NOT NULL CHECK (ends_at > starts_at),
status text NOT NULL DEFAULT 'reserved'
CHECK (status IN ('reserved','cancelled','completed')),
notes text,
created_by uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_equipment_res_space ON public.equipment_reservations (space_id);
CREATE INDEX IF NOT EXISTS idx_equipment_res_equipment ON public.equipment_reservations (equipment_id, starts_at);
CREATE INDEX IF NOT EXISTS idx_equipment_res_member ON public.equipment_reservations (member_id);
ALTER TABLE public.equipment ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.equipment_reservations ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS equipment_select ON public.equipment;
DROP POLICY IF EXISTS equipment_insert ON public.equipment;
DROP POLICY IF EXISTS equipment_update ON public.equipment;
DROP POLICY IF EXISTS equipment_delete ON public.equipment;
CREATE POLICY equipment_select ON public.equipment FOR SELECT
USING (
public.user_has_permission(auth.uid(), space_id, 'equipment.manage')
OR (
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
AND is_active
)
);
CREATE POLICY equipment_insert ON public.equipment FOR INSERT
WITH CHECK (public.user_has_permission(auth.uid(), space_id, 'equipment.manage'));
CREATE POLICY equipment_update ON public.equipment FOR UPDATE
USING (public.user_has_permission(auth.uid(), space_id, 'equipment.manage'));
CREATE POLICY equipment_delete ON public.equipment FOR DELETE
USING (public.user_has_permission(auth.uid(), space_id, 'equipment.manage'));
DROP POLICY IF EXISTS equipment_reservations_select ON public.equipment_reservations;
DROP POLICY IF EXISTS equipment_reservations_update ON public.equipment_reservations;
CREATE POLICY equipment_reservations_select ON public.equipment_reservations FOR SELECT
USING (
public.user_has_permission(auth.uid(), space_id, 'equipment.manage')
OR member_id IN (
SELECT id FROM public.space_members
WHERE user_id = auth.uid() AND space_id = equipment_reservations.space_id
)
);
CREATE POLICY equipment_reservations_update ON public.equipment_reservations FOR UPDATE
USING (public.user_has_permission(auth.uid(), space_id, 'equipment.manage'));
-- No INSERT/DELETE policy: reserve/cancel via a validated service-client action.
DROP TRIGGER IF EXISTS trg_equipment_touch ON public.equipment;
CREATE TRIGGER trg_equipment_touch
BEFORE UPDATE ON public.equipment
FOR EACH ROW EXECUTE FUNCTION public.touch_updated_at();
CREATE OR REPLACE FUNCTION public.seed_default_role_permissions()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
BEGIN
INSERT INTO public.space_role_permissions (space_id, subject, permission)
SELECT NEW.id, d.subject, d.permission
FROM (VALUES
('board','ops.kb.read'),('board','ops.kb.write'),('board','ops.process.read'),
('board','ops.process.write'),('board','ops.secrets.read'),('board','ops.secrets.write'),
('board','ops.arealeads.manage'),('board','members.manage'),('board','payments.manage'),
('board','governance.manage'),('board','forum.moderate'),('board','forms.manage'),
('board','certifications.manage'),('board','certifications.grant'),
('board','classes.manage'),('board','classes.instruct'),
('board','equipment.manage'),
('board','customize.manage'),('board','settings.manage'),
('treasurer','payments.manage'),('treasurer','ops.kb.read'),('treasurer','ops.process.read'),
('member','ops.kb.read'),('member','ops.process.read'),
('associate','ops.kb.read')
) AS d(subject, permission)
ON CONFLICT (space_id, subject, permission) DO NOTHING;
RETURN NEW;
END;
$$;
INSERT INTO public.space_role_permissions (space_id, subject, permission)
SELECT id, 'board', 'equipment.manage' FROM public.spaces
ON CONFLICT (space_id, subject, permission) DO NOTHING;
+98
View File
@@ -1829,6 +1829,7 @@ BEGIN
('board','governance.manage'),('board','forum.moderate'),('board','forms.manage'), ('board','governance.manage'),('board','forum.moderate'),('board','forms.manage'),
('board','certifications.manage'),('board','certifications.grant'), ('board','certifications.manage'),('board','certifications.grant'),
('board','classes.manage'),('board','classes.instruct'), ('board','classes.manage'),('board','classes.instruct'),
('board','equipment.manage'),
('board','customize.manage'),('board','settings.manage'), ('board','customize.manage'),('board','settings.manage'),
('treasurer','payments.manage'),('treasurer','ops.kb.read'),('treasurer','ops.process.read'), ('treasurer','payments.manage'),('treasurer','ops.kb.read'),('treasurer','ops.process.read'),
('member','ops.kb.read'),('member','ops.process.read'), ('member','ops.kb.read'),('member','ops.process.read'),
@@ -2220,3 +2221,100 @@ ON CONFLICT (space_id, subject, permission) DO NOTHING;
INSERT INTO public.space_role_permissions (space_id, subject, permission) INSERT INTO public.space_role_permissions (space_id, subject, permission)
SELECT id, 'board', 'classes.instruct' FROM public.spaces SELECT id, 'board', 'classes.instruct' FROM public.spaces
ON CONFLICT (space_id, subject, permission) DO NOTHING; ON CONFLICT (space_id, subject, permission) DO NOTHING;
-- =============================================================================
-- 20. Equipment registry + reservations
-- (equivalent to scripts/033_equipment.sql).
--
-- equipment (registry; status available/maintenance/retired; optional
-- required_certification_id -> certifications; is_active archive),
-- equipment_reservations (member time-window reservation; space_id
-- denormalized; no DB overlap constraint -- enforced in the validated
-- action). Permission equipment.manage (group Equipment). RLS additive/
-- default-deny: equipment SELECT = manager (all) or member (is_active);
-- equipment_reservations SELECT = manager (all) or member (own), UPDATE =
-- equipment.manage, NO INSERT/DELETE policy (reserve/cancel via a
-- validated service-client action enforcing status + no-overlap +
-- required-cert). No anonymous path. seed_default_role_permissions()
-- above already seeds the code; the backfill below covers existing spaces.
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.equipment (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 200),
description text,
location text,
status text NOT NULL DEFAULT 'available'
CHECK (status IN ('available','maintenance','retired')),
required_certification_id uuid REFERENCES public.certifications(id) ON DELETE SET NULL,
asset_tag text,
is_active boolean NOT NULL DEFAULT true,
created_by uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_equipment_space ON public.equipment (space_id, is_active);
CREATE TABLE IF NOT EXISTS public.equipment_reservations (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
equipment_id uuid NOT NULL REFERENCES public.equipment(id) ON DELETE CASCADE,
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
member_id uuid NOT NULL REFERENCES public.space_members(id) ON DELETE CASCADE,
starts_at timestamptz NOT NULL,
ends_at timestamptz NOT NULL CHECK (ends_at > starts_at),
status text NOT NULL DEFAULT 'reserved'
CHECK (status IN ('reserved','cancelled','completed')),
notes text,
created_by uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_equipment_res_space ON public.equipment_reservations (space_id);
CREATE INDEX IF NOT EXISTS idx_equipment_res_equipment ON public.equipment_reservations (equipment_id, starts_at);
CREATE INDEX IF NOT EXISTS idx_equipment_res_member ON public.equipment_reservations (member_id);
ALTER TABLE public.equipment ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.equipment_reservations ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS equipment_select ON public.equipment;
DROP POLICY IF EXISTS equipment_insert ON public.equipment;
DROP POLICY IF EXISTS equipment_update ON public.equipment;
DROP POLICY IF EXISTS equipment_delete ON public.equipment;
CREATE POLICY equipment_select ON public.equipment FOR SELECT
USING (
public.user_has_permission(auth.uid(), space_id, 'equipment.manage')
OR (
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
AND is_active
)
);
CREATE POLICY equipment_insert ON public.equipment FOR INSERT
WITH CHECK (public.user_has_permission(auth.uid(), space_id, 'equipment.manage'));
CREATE POLICY equipment_update ON public.equipment FOR UPDATE
USING (public.user_has_permission(auth.uid(), space_id, 'equipment.manage'));
CREATE POLICY equipment_delete ON public.equipment FOR DELETE
USING (public.user_has_permission(auth.uid(), space_id, 'equipment.manage'));
DROP POLICY IF EXISTS equipment_reservations_select ON public.equipment_reservations;
DROP POLICY IF EXISTS equipment_reservations_update ON public.equipment_reservations;
CREATE POLICY equipment_reservations_select ON public.equipment_reservations FOR SELECT
USING (
public.user_has_permission(auth.uid(), space_id, 'equipment.manage')
OR member_id IN (
SELECT id FROM public.space_members
WHERE user_id = auth.uid() AND space_id = equipment_reservations.space_id
)
);
CREATE POLICY equipment_reservations_update ON public.equipment_reservations FOR UPDATE
USING (public.user_has_permission(auth.uid(), space_id, 'equipment.manage'));
-- No INSERT/DELETE policy: reserve/cancel via a validated service-client action.
DROP TRIGGER IF EXISTS trg_equipment_touch ON public.equipment;
CREATE TRIGGER trg_equipment_touch
BEFORE UPDATE ON public.equipment
FOR EACH ROW EXECUTE FUNCTION public.touch_updated_at();
INSERT INTO public.space_role_permissions (space_id, subject, permission)
SELECT id, 'board', 'equipment.manage' FROM public.spaces
ON CONFLICT (space_id, subject, permission) DO NOTHING;
+12
View File
@@ -136,6 +136,18 @@ export type Database = {
Update: { id?: string; session_id?: string; space_id?: string; member_id?: string; status?: string; attended?: boolean; signed_up_at?: string; created_at?: string } Update: { id?: string; session_id?: string; space_id?: string; member_id?: string; status?: string; attended?: boolean; signed_up_at?: string; created_at?: string }
Relationships: [] Relationships: []
} }
equipment: {
Row: { id: string; space_id: string; name: string; description: string | null; location: string | null; status: string; required_certification_id: string | null; asset_tag: string | null; is_active: boolean; created_by: string | null; created_at: string; updated_at: string }
Insert: { id?: string; space_id: string; name: string; description?: string | null; location?: string | null; status?: string; required_certification_id?: string | null; asset_tag?: string | null; is_active?: boolean; created_by?: string | null; created_at?: string; updated_at?: string }
Update: { id?: string; space_id?: string; name?: string; description?: string | null; location?: string | null; status?: string; required_certification_id?: string | null; asset_tag?: string | null; is_active?: boolean; created_by?: string | null; created_at?: string; updated_at?: string }
Relationships: []
}
equipment_reservations: {
Row: { id: string; equipment_id: string; space_id: string; member_id: string; starts_at: string; ends_at: string; status: string; notes: string | null; created_by: string | null; created_at: string }
Insert: { id?: string; equipment_id: string; space_id: string; member_id: string; starts_at: string; ends_at: string; status?: string; notes?: string | null; created_by?: string | null; created_at?: string }
Update: { id?: string; equipment_id?: string; space_id?: string; member_id?: string; starts_at?: string; ends_at?: string; status?: string; notes?: string | null; created_by?: string | null; created_at?: string }
Relationships: []
}
activity_log: { activity_log: {
Row: { Row: {
action: string action: string