"use client"; /** * TASK-0133 — Policies tab: the FIVE org policy controls. * * require2FA — org members must complete 2FA at login * ssoEnforced — password login blocked for SSO-enabled orgs * disableGuestPublicShare — guest public shares are blocked (403 at the * share + public-access points) * maxFileSizeMB — org cap on single-file size (tightens the plan * cap; the effective value = min(org, plan)) * sessionLifetimeHours — org cap on session lifetime (non-sliding; the * effective value is MAX across the orgs you are * in, per the session contract) * allowExternalEmbeds — the authoritative embed-preview gate * * Every change is server-side `assertOrgAdmin`-gated + audited * (`org.policy.updated`). The plan cap is shown next to `maxFileSizeMB` — * the server enforces min(org, plan), so an over-cap value is clamped. */ import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { orgAdminApi, type OrgPolicy } from "@/lib/org-admin"; import { isApiError } from "@/lib/api"; import { toast } from "@/stores/ui-store"; import { Section, Field, inputCls } from "@/components/settings/settings-shell"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; const TOGGLES: { key: keyof OrgPolicy; label: string; description: string }[] = [ { key: "require2FA", label: "Require 2FA", description: "Members must complete two-factor authentication at login." }, { key: "ssoEnforced", label: "Enforce SSO", description: "Password login is blocked when an SSO provider is configured." }, { key: "disableGuestPublicShare", label: "Disable guest public shares", description: "Guests cannot create or open public shares (403)." }, { key: "allowExternalEmbeds", label: "Allow external embeds", description: "Pages can be embedded on external sites (the embed preview enforces this)." }, ]; export function AdminPoliciesTab({ orgId }: { orgId: string }) { const qc = useQueryClient(); const policy = useQuery({ queryKey: ["orgs", orgId, "admin", "policy"], queryFn: () => orgAdminApi.policy(orgId), retry: false, }); const patch = useMutation({ mutationFn: (body: Partial) => orgAdminApi.policyPatch(orgId, body), onSuccess: () => { toast("success", "Policy updated"); qc.invalidateQueries({ queryKey: ["orgs", orgId, "admin", "policy"] }); }, onError: (e) => toast("error", "Could not update policy", isApiError(e) ? e.message : undefined), }); if (policy.isLoading || !policy.data) { return

Loading policy…

; } if (policy.isError) return

Could not load the policy.

; // Keyed by the loaded policy so the draft fields re-seed after each save // (and the hooks stay unconditional inside PolicyForm). return ; } function PolicyForm({ p, patch, }: { p: { policy: OrgPolicy; effective: { maxFileSizeMB: number; billingPlan: string } }; patch: { isPending: boolean; mutate: (body: Partial) => void }; }) { const [confirm2FA, setConfirm2FA] = useState(false); const [maxFile, setMaxFile] = useState(p.policy.maxFileSizeMB); const [sessionLife, setSessionLife] = useState(p.policy.sessionLifetimeHours); return (
{TOGGLES.map((t) => (
{t.label}
{t.description}
))}
setMaxFile(e.target.value === "" ? null : Number(e.target.value))} className={inputCls} aria-label="Max file size in megabytes" />
setSessionLife(e.target.value === "" ? null : Number(e.target.value))} className={inputCls} aria-label="Session lifetime in hours" />
{ setConfirm2FA(false); patch.mutate({ require2FA: true }); }} onClose={() => setConfirm2FA(false)} />
); }