File size: 2,633 Bytes
f0743f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
 * @param email
 * @param allowedDomains
 */
export function isEmailDomainAllowed(email: string, allowedDomains?: string[] | null): boolean {
  /** If no domain restrictions are configured, allow all */
  if (!allowedDomains || !Array.isArray(allowedDomains) || !allowedDomains.length) {
    return true;
  }

  /** If restrictions exist, validate email format */
  if (!email) {
    return false;
  }

  const domain = email.split('@')[1]?.toLowerCase();

  if (!domain) {
    return false;
  }

  return allowedDomains.some((allowedDomain) => allowedDomain?.toLowerCase() === domain);
}

/**
 * Normalizes a domain string. If the domain is invalid, returns null.
 * Normalized === lowercase, trimmed, and protocol added if missing.
 * @param domain
 */
function normalizeDomain(domain: string): string | null {
  try {
    let normalizedDomain = domain.toLowerCase().trim();

    // Early return for obviously invalid formats
    if (normalizedDomain === 'http://' || normalizedDomain === 'https://') {
      return null;
    }

    // If it's not already a URL, make it one
    if (!normalizedDomain.startsWith('http://') && !normalizedDomain.startsWith('https://')) {
      normalizedDomain = `https://${normalizedDomain}`;
    }

    const url = new URL(normalizedDomain);
    // Additional validation that hostname isn't just protocol
    if (!url.hostname || url.hostname === 'http:' || url.hostname === 'https:') {
      return null;
    }

    return url.hostname.replace(/^www\./i, '');
  } catch {
    return null;
  }
}

/**
 * Checks if the given domain is allowed. If no restrictions are set, allows all domains.
 * @param domain
 * @param allowedDomains
 */
export async function isActionDomainAllowed(
  domain?: string | null,
  allowedDomains?: string[] | null,
): Promise<boolean> {
  if (!domain || typeof domain !== 'string') {
    return false;
  }

  if (!Array.isArray(allowedDomains) || !allowedDomains.length) {
    return true;
  }

  const normalizedInputDomain = normalizeDomain(domain);
  if (!normalizedInputDomain) {
    return false;
  }

  for (const allowedDomain of allowedDomains) {
    const normalizedAllowedDomain = normalizeDomain(allowedDomain);
    if (!normalizedAllowedDomain) {
      continue;
    }

    if (normalizedAllowedDomain.startsWith('*.')) {
      const baseDomain = normalizedAllowedDomain.slice(2);
      if (
        normalizedInputDomain === baseDomain ||
        normalizedInputDomain.endsWith(`.${baseDomain}`)
      ) {
        return true;
      }
    } else if (normalizedInputDomain === normalizedAllowedDomain) {
      return true;
    }
  }

  return false;
}