Files
accounted/components/settings/members-payload.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

83 lines
3.1 KiB
TypeScript

/**
* Read-side payload guards for the member rosters
* (CompanyMembersSection.tsx, TeamPanel.tsx).
*
* A 200 whose body does not carry the expected lists is a failed read, not an
* empty roster: returning null forces the caller into its error state instead
* of rendering an apparently member-less company or team. A consultant who
* sees an empty list re-invites people who are already members, so "cannot
* read the roster" must never render as "no members".
*
* The element types are the caller's own row interfaces; these guards verify
* the envelope shape (lists are lists, flags are booleans), not the fields of
* each row.
*/
export interface CompanyMembersPayload<M, I> {
members: M[]
invitations: I[]
canInvite: boolean
/** Multi-user seat gate: swap the invite form for the paid-plan upsell. */
inviteRequiresUpgrade: boolean
}
export function parseCompanyMembersPayload<M, I>(body: unknown): CompanyMembersPayload<M, I> | null {
if (typeof body !== 'object' || body === null) return null
const data = (body as { data?: unknown }).data
if (typeof data !== 'object' || data === null) return null
const d = data as {
members?: unknown
invitations?: unknown
canInvite?: unknown
inviteRequiresUpgrade?: unknown
}
if (!Array.isArray(d.members) || !Array.isArray(d.invitations)) return null
return {
members: d.members as M[],
invitations: d.invitations as I[],
// Only an explicit boolean true unlocks the invite form.
canInvite: d.canInvite === true,
// Only an explicit true swaps the form for the upsell: an older server
// without the field keeps the form, and the POST's own 403 still guards.
inviteRequiresUpgrade: d.inviteRequiresUpgrade === true,
}
}
export interface TeamMembersPayload<M, I> {
members: M[]
invitations: I[]
teamName: string | null
teamId: string | null
teamKind: string | null
isOwner: boolean
canInvite: boolean
}
export function parseTeamMembersPayload<M, I>(body: unknown): TeamMembersPayload<M, I> | null {
if (typeof body !== 'object' || body === null) return null
const data = (body as { data?: unknown }).data
if (typeof data !== 'object' || data === null) return null
const d = data as {
members?: unknown
invitations?: unknown
teamName?: unknown
teamId?: unknown
teamKind?: unknown
isOwner?: unknown
canInvite?: unknown
}
if (!Array.isArray(d.members)) return null
return {
members: d.members as M[],
// Management-only extra, not roster state: a missing list means "none",
// not a failed read (the strictness above protects the member roster).
invitations: Array.isArray(d.invitations) ? (d.invitations as I[]) : [],
teamName: typeof d.teamName === 'string' && d.teamName.length > 0 ? d.teamName : null,
teamId: typeof d.teamId === 'string' && d.teamId.length > 0 ? d.teamId : null,
teamKind: typeof d.teamKind === 'string' && d.teamKind.length > 0 ? d.teamKind : null,
// Only explicit boolean true unlocks management affordances.
isOwner: d.isOwner === true,
canInvite: d.canInvite === true,
}
}