feat: import row validation + KB/process per-item ACLs
Continuing the audit backlog and finishing the permissions UI. Import validation (recurring audit finding): - Added importMembersSchema / importPaymentsCsvSchema. importMembers and importPaymentsCsv now validate per row (a single bad row no longer rejects the whole file), lowercase emails, normalize dates through flexibleDateTime(), enforce tier/platform enums and positive finite amounts, and return a `skipped` count instead of silently dropping invalid rows. Permissions UI completion: - OpsAclEditor is now wired into the KB/process viewer modal (admin/board), with entity type derived from the `process` tag. Per-item Ops access control now covers secrets + KB + processes, matching "all ops facilities stuff". Left as-is with rationale in HANDOFF: /ops + /import denial paths (unreachable behind the layout gate or already a clear non-leaking denial); members-row make-area-lead shortcut (Customize -> Area leads fully covers assignment); types/database.ts + docs regen (pure hygiene, zero runtime/security impact, still tracked). 271/271 tests pass. Build clean. No migration this pass.
This commit is contained in:
@@ -522,6 +522,32 @@ export const assignAreaLeadSchema = z.object({
|
||||
member_id: z.string().uuid('Invalid member ID'),
|
||||
})
|
||||
|
||||
// ─── Bulk import ─────────────────────────────────────────────────────────────
|
||||
|
||||
const importEmail = z.string().email('Invalid email').max(200).transform(s => s.trim().toLowerCase())
|
||||
|
||||
export const importMembersSchema = z.array(
|
||||
z.object({
|
||||
display_name: z.string().min(1, 'Name required').max(100),
|
||||
email: importEmail,
|
||||
phone: z.string().max(20).optional().nullable(),
|
||||
tier: z.enum(['plus', 'basic', 'associate']).optional(),
|
||||
joined_at: flexibleDateTime().optional(),
|
||||
last_paid_at: flexibleDateTime().optional(),
|
||||
has_card_access: z.boolean().optional(),
|
||||
}),
|
||||
).max(5000)
|
||||
|
||||
export const importPaymentsCsvSchema = z.array(
|
||||
z.object({
|
||||
platform: z.enum(['paypal', 'zeffy', 'venmo', 'cash']),
|
||||
amount: z.number().finite().positive('Amount must be positive').max(10_000_000),
|
||||
from_identifier: z.string().min(1, 'Payer required').max(200),
|
||||
from_note: z.string().max(500).optional().nullable(),
|
||||
transaction_date: flexibleDateTime().optional(),
|
||||
}),
|
||||
).max(10000)
|
||||
|
||||
// ─── Generic ID schemas ──────────────────────────────────────────────────────
|
||||
|
||||
export const uuidSchema = z.string().uuid('Invalid ID format')
|
||||
|
||||
Reference in New Issue
Block a user