Per-file type-only fixes surfaced after the Supabase client was correctly typed. All fixes preserve runtime behavior; no any/ts-ignore/ts-expect-error. - lib/supabase/client.ts: annotate the browser client as SupabaseClient<Database> (same ssr/js drift as server.ts; fixes it centrally for all callers). - Supabase embedded-select joins whose FK metadata is missing from the generated types are bridged with `as unknown as <target>` + a TODO to remove after regenerating types/database.ts. - setState optimistic-update variance and raw <select> enum values cast to the precise element/union type; .insert payload fields (Json, enums) given the precise column type via satisfies/field casts. - Added narrowing guards where a value was legitimately nullable (comms channel, public form page) without changing the intended path. The 14 remaining errors are confined to the four files fixed in Phase B (real bugs + the tautological actions.test rewrite). 639 tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
66 lines
3.2 KiB
TypeScript
66 lines
3.2 KiB
TypeScript
import { createClient } from '@/lib/supabase/server'
|
|
import { OpsClient } from './ops-client'
|
|
import type { Tables } from '@/types/database'
|
|
|
|
export default async function OpsPage() {
|
|
const supabase = await createClient()
|
|
const { data: { user } } = await supabase.auth.getUser()
|
|
if (!user) return null
|
|
|
|
const { data: member } = await supabase
|
|
.from('space_members').select('space_id, role, display_name, user_id').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
|
|
if (!member) return null
|
|
|
|
const isAdminBoard = member.role === 'admin' || member.role === 'board'
|
|
// Writing/managing secrets and the ACL stays admin/board only.
|
|
const canManageAcl = isAdminBoard
|
|
|
|
// The secrets list is always queried; secrets_select RLS filters it to the
|
|
// rows this member may see (admin/board, OR the ops.secrets.read role
|
|
// permission, OR a per-secret ops_acl entry). Only metadata is selected
|
|
// here; plaintext is fetched on demand via revealSecret().
|
|
const [{ data: kbEntries }, { data: areaLeads }, secretsResult, { data: customRoles }, { data: acl }, { data: canReadSecretsPerm }] = await Promise.all([
|
|
supabase.from('knowledge_base').select('*').eq('space_id', member.space_id).order('is_pinned', { ascending: false }).order('created_at', { ascending: false }),
|
|
supabase.from('area_leads').select('*').eq('space_id', member.space_id).order('area_name'),
|
|
supabase.from('secrets').select('id, title, area, created_at, label, description, icon, space_id, created_by, updated_at, category, notes, encryption_version').eq('space_id', member.space_id),
|
|
supabase.from('space_custom_roles').select('slug, name').eq('space_id', member.space_id),
|
|
supabase.from('ops_acl').select('entity_type, entity_id, role').eq('space_id', member.space_id),
|
|
supabase.rpc('user_has_permission', { uid: user.id, sid: member.space_id, perm: 'ops.secrets.read' }),
|
|
])
|
|
|
|
// Show the Secrets section to admin/board, to a holder of the
|
|
// ops.secrets.read permission, or to anyone RLS returned at least one
|
|
// (ACL-granted) secret for.
|
|
const canSeeSecrets = isAdminBoard || !!canReadSecretsPerm || (secretsResult.data?.length ?? 0) > 0
|
|
|
|
// Selectable ACL roles: built-in roles, custom-role slugs, and area-lead
|
|
// sentinels labelled by area name.
|
|
const aclRoleOptions = [
|
|
{ value: 'treasurer', label: 'treasurer' },
|
|
{ value: 'member', label: 'member' },
|
|
{ value: 'associate', label: 'associate' },
|
|
...((customRoles ?? []).map(c => ({ value: c.slug, label: c.name }))),
|
|
...((areaLeads ?? []).map(al => ({ value: `area_lead:${al.id}`, label: `${al.area_name} lead` }))),
|
|
]
|
|
|
|
const aclByEntity: Record<string, string[]> = {}
|
|
for (const row of acl ?? []) {
|
|
const k = `${row.entity_type}:${row.entity_id}`
|
|
;(aclByEntity[k] ??= []).push(row.role)
|
|
}
|
|
|
|
return (
|
|
<OpsClient
|
|
member={member as Tables<'space_members'>}
|
|
spaceId={member.space_id}
|
|
kbEntries={kbEntries ?? []}
|
|
areaLeads={areaLeads ?? []}
|
|
secrets={(secretsResult.data ?? []) as Tables<'secrets'>[]}
|
|
canSeeSecrets={canSeeSecrets}
|
|
canManageAcl={canManageAcl}
|
|
aclRoleOptions={aclRoleOptions}
|
|
aclByEntity={aclByEntity}
|
|
/>
|
|
)
|
|
}
|