InviteMemberButton.utils.ts109 lines · main
1import * as z from 'zod'
2
3import type { OrganizationMember } from '@/data/organizations/organization-members-query'
4
5export const MAX_BATCH_INVITE_SIZE = 50
6
7/** Max characters to show when an invalid token is long (e.g. comma-less paste of many addresses). */
8const MAX_INVALID_EMAIL_SNIPPET_LENGTH = 120
9
10function formatInvalidEmailSnippet(token: string): string {
11 if (token.length <= MAX_INVALID_EMAIL_SNIPPET_LENGTH) return token
12 return `${token.slice(0, MAX_INVALID_EMAIL_SNIPPET_LENGTH)}…`
13}
14
15export const emailSchema = z
16 .string()
17 .min(1, 'At least one email address is required')
18 .refine(
19 (val) => {
20 const emails = parseEmails(val)
21 if (emails.length === 0) return false
22 return emails.every((e) => z.string().email().safeParse(e).success)
23 },
24 (val) => {
25 const emails = parseEmails(val)
26 const invalid = emails.find((e) => !z.string().email().safeParse(e).success)
27 return {
28 message: invalid
29 ? `Invalid email address: "${formatInvalidEmailSnippet(invalid)}"`
30 : 'At least one email address is required',
31 }
32 }
33 )
34 .refine(
35 (val) => parseEmails(val).length <= MAX_BATCH_INVITE_SIZE,
36 (val) => {
37 const count = parseEmails(val).length
38 return {
39 message: `You can invite up to ${MAX_BATCH_INVITE_SIZE} members at a time. Remove ${count - MAX_BATCH_INVITE_SIZE} email ${count - MAX_BATCH_INVITE_SIZE === 1 ? 'address' : 'addresses'} to continue.`,
40 }
41 }
42 )
43
44export function parseEmails(value: string): string[] {
45 const emails = value
46 .split(/[\s,]+/)
47 .map((e) => e.trim().toLowerCase())
48 .filter(Boolean)
49 return [...new Set(emails)]
50}
51
52export type CategorizedEmails = {
53 alreadyInvited: string[]
54 alreadyMembers: string[]
55 toInvite: string[]
56}
57
58export type BatchInvitationFailure = {
59 email: string
60 error: string
61}
62
63export type BatchInvitationResult = {
64 succeeded: string[]
65 failed: BatchInvitationFailure[]
66}
67
68export function categorizeInviteEmails(
69 emails: string[],
70 members: OrganizationMember[]
71): CategorizedEmails {
72 const alreadyInvited: string[] = []
73 const alreadyMembers: string[] = []
74 const toInvite: string[] = []
75
76 for (const email of emails) {
77 const existingMember = members.find((m) => m.primary_email === email)
78 if (existingMember !== undefined) {
79 if (existingMember.invited_id) {
80 alreadyInvited.push(email)
81 } else {
82 alreadyMembers.push(email)
83 }
84 } else {
85 toInvite.push(email)
86 }
87 }
88
89 return { alreadyInvited, alreadyMembers, toInvite }
90}
91
92export function buildProjectPayload(
93 applyToOrg: boolean,
94 projectRef: string
95): { projects: string[] } | Record<string, never> {
96 if (applyToOrg) return {}
97 if (!projectRef) {
98 throw new Error('projectRef is required when applyToOrg is false')
99 }
100 return { projects: [projectRef] }
101}
102
103export function buildSsoPayload(
104 requireSso: 'auto' | 'sso' | 'non-sso'
105): { requireSso: boolean } | Record<string, never> {
106 if (requireSso === 'sso') return { requireSso: true }
107 if (requireSso === 'non-sso') return { requireSso: false }
108 return {}
109}