* feat(peppol): gate Peppol per company: request access, operator enables with a sending cap Peppol is no longer available to every company by default. Each transmission is billed per document by the access point and each receiving identifier consumes a contracted tenant slot, so the product now works like this: - peppol_access (new table, RLS read-only for members, service-role writes): status requested | enabled | disabled, max_sends (null = no cap), receive_enabled as a separate grant, who asked and who enabled. - POST /api/settings/peppol/access: the company asks from Settings > Fakturering; the row is written and the operators are e-mailed (best effort, the row is the source of truth). - scripts/peppol/access.ts list | enable <company|orgnr> [--max-sends N] [--receive] | disable | show: the operator side. - POST /api/invoices/[id]/peppol/send refuses PEPPOL_ACCESS_REQUIRED / PEPPOL_SEND_LIMIT_REACHED before touching the invoice; the invoice page's send item says so instead of pretending. Registration for receiving refuses PEPPOL_ACCESS_REQUIRED / PEPPOL_RECEIVING_NOT_ENABLED. - Settings UI: access status row with "Begär åtkomst", sends used of cap, receiving switch only once receiving is granted. Refs #546 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ * test(peppol): pass route params to the settings handlers; baseline-align the access row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ * fix(peppol): revoke default table privileges from authenticated on the access and receiving tables Supabase grants ALL on new tables to authenticated by default; the earlier REVOKE covered PUBLIC and anon only, so a member's UPDATE on peppol_access was an RLS-filtered no-op instead of a permission error (pg-real caught it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
62 lines
3.1 KiB
SQL
62 lines
3.1 KiB
SQL
-- Peppol access is granted per company, never self-served (#546).
|
|
--
|
|
-- Every Peppol transmission costs money at the Access Point and every
|
|
-- receiving identifier consumes a contracted tenant slot, so a company is
|
|
-- locked out by default, asks for access from the settings page, and the
|
|
-- operators enable it (with a sending cap) from the service role. The table
|
|
-- is readable by the company's members and written by nobody but the service
|
|
-- role: the browser can ask, it can never grant itself anything.
|
|
|
|
CREATE TABLE public.peppol_access (
|
|
company_id uuid PRIMARY KEY REFERENCES public.companies(id) ON DELETE CASCADE,
|
|
status text NOT NULL DEFAULT 'requested'
|
|
CHECK (status IN ('requested', 'enabled', 'disabled')),
|
|
-- Cap on transmissions through the access point; null = no cap.
|
|
max_sends integer CHECK (max_sends IS NULL OR max_sends >= 0),
|
|
-- Receiving (publishing the company's identifier) is a separate, scarcer
|
|
-- grant: it consumes one of the contracted tenant slots.
|
|
receive_enabled boolean NOT NULL DEFAULT false,
|
|
requested_at timestamptz,
|
|
requested_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
request_note text CHECK (request_note IS NULL OR length(request_note) <= 2000),
|
|
enabled_at timestamptz,
|
|
-- Free-text label of who granted it (operator e-mail or script name);
|
|
-- operators are not application users, so no FK.
|
|
enabled_by text,
|
|
disabled_at timestamptz,
|
|
note text,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT peppol_access_status_shape CHECK (
|
|
(status = 'enabled' AND enabled_at IS NOT NULL)
|
|
OR (status = 'disabled' AND disabled_at IS NOT NULL)
|
|
OR status = 'requested'
|
|
)
|
|
);
|
|
|
|
CREATE INDEX peppol_access_status_idx ON public.peppol_access (status, requested_at);
|
|
|
|
CREATE TRIGGER set_peppol_access_updated_at
|
|
BEFORE UPDATE ON public.peppol_access
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
ALTER TABLE public.peppol_access ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "view own-company peppol access"
|
|
ON public.peppol_access FOR SELECT
|
|
USING (company_id IN (SELECT public.user_company_ids()));
|
|
-- Supabase's default privileges hand every new table ALL to authenticated;
|
|
-- take that back so a member's UPDATE is a hard "permission denied" rather
|
|
-- than an RLS-filtered no-op, then grant the one thing members need.
|
|
REVOKE ALL ON public.peppol_access FROM PUBLIC, anon, authenticated;
|
|
GRANT SELECT ON public.peppol_access TO authenticated;
|
|
|
|
-- Same tightening for the two receiving tables from 20260821170000, which
|
|
-- revoked from PUBLIC and anon only (their RLS already blocked writes; this
|
|
-- makes the refusal explicit at the privilege level too).
|
|
REVOKE ALL ON public.peppol_registrations FROM PUBLIC, anon, authenticated;
|
|
GRANT SELECT ON public.peppol_registrations TO authenticated;
|
|
REVOKE ALL ON public.peppol_inbound_documents FROM PUBLIC, anon, authenticated;
|
|
GRANT SELECT ON public.peppol_inbound_documents TO authenticated;
|
|
|
|
NOTIFY pgrst, 'reload schema';
|