@koloseum/utils 0.2.28 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +125 -0
- package/dist/client.js +680 -0
- package/dist/formatting.d.ts +125 -0
- package/dist/formatting.js +369 -0
- package/dist/general.d.ts +54 -0
- package/dist/general.js +88 -0
- package/dist/platform.d.ts +66 -0
- package/dist/platform.js +764 -0
- package/dist/server.d.ts +77 -0
- package/dist/server.js +300 -0
- package/package.json +25 -3
- package/dist/utils.d.ts +0 -394
- package/dist/utils.js +0 -2019
package/dist/utils.js
DELETED
|
@@ -1,2019 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
import { error as svelteError } from "@sveltejs/kit";
|
|
3
|
-
import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from "@supabase/supabase-js";
|
|
4
|
-
import Bowser from "bowser";
|
|
5
|
-
import validator from "validator";
|
|
6
|
-
/* HELPERS */
|
|
7
|
-
const parsePgInterval = (await import("postgres-interval")).default;
|
|
8
|
-
const sanitize = (await import("sanitize-html")).default;
|
|
9
|
-
const { trim, escape, isMobilePhone, isURL } = validator;
|
|
10
|
-
const { KenyaAdministrativeDivisions } = (await import("kenya-administrative-divisions")).default;
|
|
11
|
-
/* DUMMY DATA */
|
|
12
|
-
export const Data = {
|
|
13
|
-
/**
|
|
14
|
-
* A generic authenticated user.
|
|
15
|
-
* @param {string} id - The user ID; defaults to a random UUID
|
|
16
|
-
* @param {Date} date - The date and time; defaults to the current date and time
|
|
17
|
-
* @param {string} phone - The phone number; defaults to a generic Kenyan phone number
|
|
18
|
-
* @param {string} identityId - A default identity ID; defaults to a random UUID
|
|
19
|
-
* @returns A generic authenticated user.
|
|
20
|
-
*/
|
|
21
|
-
authenticatedUser: (id = uuidv4(), date = new Date(), phone = "254111222333", identityId = uuidv4()) => {
|
|
22
|
-
// Convert date to ISO string
|
|
23
|
-
const time = date.toISOString();
|
|
24
|
-
// User data
|
|
25
|
-
return {
|
|
26
|
-
id,
|
|
27
|
-
aud: "authenticated",
|
|
28
|
-
role: "authenticated",
|
|
29
|
-
email: "",
|
|
30
|
-
phone,
|
|
31
|
-
phone_confirmed_at: time,
|
|
32
|
-
confirmation_sent_at: time,
|
|
33
|
-
confirmed_at: time,
|
|
34
|
-
last_sign_in_at: time,
|
|
35
|
-
app_metadata: {
|
|
36
|
-
person_data: { player_id: "KP1234567", first_name: "John", last_name: "Test", pseudonym: "JDtest" },
|
|
37
|
-
provider: "phone",
|
|
38
|
-
providers: ["phone"],
|
|
39
|
-
roles: ["player", "backroom_superuser"]
|
|
40
|
-
},
|
|
41
|
-
user_metadata: {
|
|
42
|
-
email_verified: false,
|
|
43
|
-
phone_verified: false,
|
|
44
|
-
sub: id,
|
|
45
|
-
backroom: {
|
|
46
|
-
welcome_notification_sent: true
|
|
47
|
-
},
|
|
48
|
-
players: {
|
|
49
|
-
welcome_notification_sent: true
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
identities: [
|
|
53
|
-
{
|
|
54
|
-
identity_id: identityId,
|
|
55
|
-
id,
|
|
56
|
-
user_id: id,
|
|
57
|
-
identity_data: {
|
|
58
|
-
email_verified: false,
|
|
59
|
-
phone_verified: false,
|
|
60
|
-
sub: id
|
|
61
|
-
},
|
|
62
|
-
provider: "phone",
|
|
63
|
-
last_sign_in_at: time,
|
|
64
|
-
created_at: time,
|
|
65
|
-
updated_at: time
|
|
66
|
-
}
|
|
67
|
-
],
|
|
68
|
-
created_at: time,
|
|
69
|
-
updated_at: time,
|
|
70
|
-
is_anonymous: false
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
/* STATUS MESSAGES */
|
|
75
|
-
export const Status = {
|
|
76
|
-
/**
|
|
77
|
-
* A generic error message.
|
|
78
|
-
*/
|
|
79
|
-
ERROR: "An unexpected error occurred. Kindly try again.",
|
|
80
|
-
/**
|
|
81
|
-
* A generic loading message.
|
|
82
|
-
*/
|
|
83
|
-
LOADING: "Please wait…",
|
|
84
|
-
/**
|
|
85
|
-
* A generic password reset request message.
|
|
86
|
-
*/
|
|
87
|
-
PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly."
|
|
88
|
-
};
|
|
89
|
-
/* UTILITY FUNCTIONS */
|
|
90
|
-
export const Utility = {
|
|
91
|
-
/**
|
|
92
|
-
* Calls a Supabase Edge function.
|
|
93
|
-
* @param {boolean} browser - Whether the function is being called in the browser
|
|
94
|
-
* @param {SupabaseClient<Database>} supabase - The Supabase client
|
|
95
|
-
* @param {string} path - The path to the Edge function
|
|
96
|
-
* @param {FunctionInvokeOptions} [options] - The options to use for the function invocation; defaults to `{ method: "GET" }`
|
|
97
|
-
* @returns The response from the Edge function
|
|
98
|
-
*/
|
|
99
|
-
callEdgeFunction: async (browser, supabase, path, options = { method: "GET" }) => {
|
|
100
|
-
try {
|
|
101
|
-
// Only add Origin header in browser environment
|
|
102
|
-
if (browser)
|
|
103
|
-
options.headers = { ...options.headers, Origin: window.location.origin };
|
|
104
|
-
// Fetch response with additional options for better cross-origin handling
|
|
105
|
-
const { data, error } = await supabase.functions.invoke(path, options);
|
|
106
|
-
// Handle different error types
|
|
107
|
-
if (error) {
|
|
108
|
-
// Define context
|
|
109
|
-
const context = {};
|
|
110
|
-
// Define error code and message
|
|
111
|
-
const code = Number(error.context?.status) || 500;
|
|
112
|
-
let message = error.context?.statusText || Status.ERROR;
|
|
113
|
-
// Handle HTTP errors
|
|
114
|
-
if (error instanceof FunctionsHttpError) {
|
|
115
|
-
try {
|
|
116
|
-
// Get content type header safely
|
|
117
|
-
const contentType = error.context?.headers?.get("content-type") || "";
|
|
118
|
-
// If content type is JSON, get error data
|
|
119
|
-
if (contentType.includes("application/json")) {
|
|
120
|
-
try {
|
|
121
|
-
const errorData = await error.context.json();
|
|
122
|
-
message = errorData.message || message;
|
|
123
|
-
// Assign attempt ID to context if present
|
|
124
|
-
if (path.includes("verify-id") && errorData.attemptId)
|
|
125
|
-
context.attemptId = errorData.attemptId;
|
|
126
|
-
}
|
|
127
|
-
catch (jsonError) {
|
|
128
|
-
console.error("Failed to parse JSON error response:", jsonError);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// If content type is plain text, get error message
|
|
132
|
-
else if (contentType.includes("text/plain")) {
|
|
133
|
-
try {
|
|
134
|
-
message = await error.context.text();
|
|
135
|
-
}
|
|
136
|
-
catch (textError) {
|
|
137
|
-
console.error("Failed to parse text error response:", textError);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (parseError) {
|
|
142
|
-
console.error("Failed to parse error response:", parseError);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// Handle relay and fetch errors
|
|
146
|
-
else if (error instanceof FunctionsRelayError || error instanceof FunctionsFetchError)
|
|
147
|
-
message = error.message;
|
|
148
|
-
// Return error
|
|
149
|
-
return { code: code, error: message, context };
|
|
150
|
-
}
|
|
151
|
-
// Return response
|
|
152
|
-
return { code: 200, data };
|
|
153
|
-
}
|
|
154
|
-
catch (unexpectedError) {
|
|
155
|
-
console.error("Unexpected error:", unexpectedError);
|
|
156
|
-
return { code: 500, error: Status.ERROR };
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
/**
|
|
160
|
-
* Capitalises a given word.
|
|
161
|
-
* @param {string} word - The word to be capitalised
|
|
162
|
-
* @returns The capitalised word
|
|
163
|
-
*/
|
|
164
|
-
capitalise: (word) => word.charAt(0).toUpperCase() +
|
|
165
|
-
word
|
|
166
|
-
.slice(1)
|
|
167
|
-
.split("")
|
|
168
|
-
.map((c) => c.toLowerCase())
|
|
169
|
-
.join(""),
|
|
170
|
-
/**
|
|
171
|
-
* Cleans a URL by removing the protocol and trailing slashes.
|
|
172
|
-
* @param {string} url - The URL to clean
|
|
173
|
-
* @returns {string} The cleaned URL
|
|
174
|
-
*/
|
|
175
|
-
cleanUrl: (url) => url.replace(/^https?:\/\//, "").replace(/\/+$/, ""),
|
|
176
|
-
/**
|
|
177
|
-
* Returns a custom error object.
|
|
178
|
-
* @param {number} code - The error code; defaults to `500` if not provided or invalid
|
|
179
|
-
* @param {string} message - The error message
|
|
180
|
-
* @returns An object with the error `code` and `message`
|
|
181
|
-
*/
|
|
182
|
-
customError: (code, message) => !code || code < 400 || code > 599 ? { code: 500, message: Status.ERROR } : { code, message },
|
|
183
|
-
/**
|
|
184
|
-
* A regular expression for E.164 phone numbers.
|
|
185
|
-
*
|
|
186
|
-
* - Source: https://www.twilio.com/docs/glossary/what-e164
|
|
187
|
-
*/
|
|
188
|
-
e164Regex: /^\+?[1-9]\d{1,14}$/,
|
|
189
|
-
/**
|
|
190
|
-
* Formats a date in the format DD/MM/YYYY (by default) or YYYY-MM-DD (for client).
|
|
191
|
-
* @param {string} date - Date to be formatted
|
|
192
|
-
* @param {boolean} forClient - Specify whether the data is for displaying on the client; defaults to `false`
|
|
193
|
-
* @returns The formatted date string, or `null` if invalid input or in case of an error
|
|
194
|
-
*/
|
|
195
|
-
formatDate: (date, forClient = false) => {
|
|
196
|
-
// Return null if no date is provided
|
|
197
|
-
if (date === "" || typeof date !== "string")
|
|
198
|
-
return null;
|
|
199
|
-
// Handle input
|
|
200
|
-
try {
|
|
201
|
-
// Parse date string
|
|
202
|
-
const parsedDate = Date.parse(date);
|
|
203
|
-
// Return null if date string is not parseable
|
|
204
|
-
if (isNaN(parsedDate))
|
|
205
|
-
return null;
|
|
206
|
-
// Use the Intl.DateTimeFormat with explicit options to ensure correct formatting
|
|
207
|
-
const formattedDate = new Intl.DateTimeFormat(forClient ? "fr-CA" : "en-KE", {
|
|
208
|
-
day: "2-digit",
|
|
209
|
-
month: "2-digit",
|
|
210
|
-
year: "numeric",
|
|
211
|
-
timeZone: "Africa/Nairobi"
|
|
212
|
-
}).format(parsedDate);
|
|
213
|
-
// Return formatted date string
|
|
214
|
-
return formattedDate;
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
console.error(error);
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
/**
|
|
222
|
-
* Formats a social media platform for display.
|
|
223
|
-
* @param {SocialMediaPlatform} platform - The social media platform to be formatted
|
|
224
|
-
* @returns The formatted social media platform string
|
|
225
|
-
*/
|
|
226
|
-
formatSocialMediaPlatform: (platform) => {
|
|
227
|
-
const platforms = ["facebook", "instagram", "kick", "tiktok", "twitch", "twitter", "youtube"];
|
|
228
|
-
if (!platforms.includes(platform))
|
|
229
|
-
return "";
|
|
230
|
-
if (platform === "tiktok")
|
|
231
|
-
return "TikTok";
|
|
232
|
-
if (platform === "twitter")
|
|
233
|
-
return "X (formerly Twitter)";
|
|
234
|
-
if (platform === "youtube")
|
|
235
|
-
return "YouTube";
|
|
236
|
-
return Utility.capitalise(platform);
|
|
237
|
-
},
|
|
238
|
-
/**
|
|
239
|
-
* Returns an age in years given a birth date.
|
|
240
|
-
* @param {Date | string | number | null} dateOfBirth - The date of birth
|
|
241
|
-
* @returns The age in years, or `NaN` if the input is invalid
|
|
242
|
-
*/
|
|
243
|
-
getAge: (dateOfBirth) => {
|
|
244
|
-
// Return NaN if no date is provided
|
|
245
|
-
if (!dateOfBirth)
|
|
246
|
-
return NaN;
|
|
247
|
-
// Validate and process date of birth
|
|
248
|
-
let birthDate;
|
|
249
|
-
if (dateOfBirth instanceof Date) {
|
|
250
|
-
if (isNaN(dateOfBirth.getTime()))
|
|
251
|
-
return NaN;
|
|
252
|
-
birthDate = dateOfBirth;
|
|
253
|
-
}
|
|
254
|
-
else if (typeof dateOfBirth === "string") {
|
|
255
|
-
if (dateOfBirth === "" || isNaN(Date.parse(dateOfBirth)))
|
|
256
|
-
return NaN;
|
|
257
|
-
// Format date of birth if it's a string
|
|
258
|
-
const formattedDate = Utility.formatDate(dateOfBirth, true);
|
|
259
|
-
if (!formattedDate)
|
|
260
|
-
return NaN;
|
|
261
|
-
birthDate = new Date(formattedDate);
|
|
262
|
-
}
|
|
263
|
-
else if (typeof dateOfBirth === "number") {
|
|
264
|
-
if (isNaN(dateOfBirth))
|
|
265
|
-
return NaN;
|
|
266
|
-
birthDate = new Date(dateOfBirth);
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
return NaN;
|
|
270
|
-
}
|
|
271
|
-
// Validate the final date object
|
|
272
|
-
if (isNaN(birthDate.getTime()))
|
|
273
|
-
return NaN;
|
|
274
|
-
// Calculate age
|
|
275
|
-
const currentDate = new Date();
|
|
276
|
-
const monthDiff = currentDate.getMonth() - birthDate.getMonth();
|
|
277
|
-
let age = currentDate.getFullYear() - birthDate.getFullYear();
|
|
278
|
-
// Return age
|
|
279
|
-
return monthDiff < 0 || (monthDiff === 0 && currentDate.getDate() < birthDate.getDate()) ? --age : age;
|
|
280
|
-
},
|
|
281
|
-
/**
|
|
282
|
-
* Formats a date as a locale-specific string.
|
|
283
|
-
* @param {Date | string | number | null} date - Date to be formatted
|
|
284
|
-
* @param {boolean} withTime - Specify whether the date should include time; defaults to `false`
|
|
285
|
-
* @param {string} timeZone - The time zone to use for formatting; defaults to `Africa/Nairobi`
|
|
286
|
-
* @returns The formatted date string, or an empty string if invalid input
|
|
287
|
-
*/
|
|
288
|
-
getDateString: (date, withTime = false, timeZone = "Africa/Nairobi") => {
|
|
289
|
-
// Return empty string if no date is provided
|
|
290
|
-
if (!date)
|
|
291
|
-
return "";
|
|
292
|
-
// Validate date to format
|
|
293
|
-
let dateToFormat;
|
|
294
|
-
if (date instanceof Date) {
|
|
295
|
-
if (isNaN(date.getTime()))
|
|
296
|
-
return "";
|
|
297
|
-
dateToFormat = date;
|
|
298
|
-
}
|
|
299
|
-
else if (typeof date === "string" || typeof date === "number") {
|
|
300
|
-
if (typeof date === "string" && (date === "" || isNaN(Date.parse(date))))
|
|
301
|
-
return "";
|
|
302
|
-
if (typeof date === "number" && isNaN(date))
|
|
303
|
-
return "";
|
|
304
|
-
dateToFormat = new Date(date);
|
|
305
|
-
}
|
|
306
|
-
else
|
|
307
|
-
return "";
|
|
308
|
-
// Validate the final date object
|
|
309
|
-
if (isNaN(dateToFormat.getTime()))
|
|
310
|
-
return "";
|
|
311
|
-
// Format date
|
|
312
|
-
return dateToFormat.toLocaleString("en-KE", {
|
|
313
|
-
timeZone,
|
|
314
|
-
year: "numeric",
|
|
315
|
-
month: "long",
|
|
316
|
-
day: "numeric",
|
|
317
|
-
hour: withTime ? "numeric" : undefined,
|
|
318
|
-
minute: withTime ? "2-digit" : undefined,
|
|
319
|
-
hour12: withTime ? true : undefined
|
|
320
|
-
});
|
|
321
|
-
},
|
|
322
|
-
/**
|
|
323
|
-
* Formats an identity type for display.
|
|
324
|
-
* @param {IdentityType} identityType - The identity type to be formatted
|
|
325
|
-
* @returns The formatted identity type string
|
|
326
|
-
*/
|
|
327
|
-
getIdentityTypeString: (identityType) => {
|
|
328
|
-
if (identityType === "national")
|
|
329
|
-
return "National ID";
|
|
330
|
-
if (identityType === "alien")
|
|
331
|
-
return "Alien ID";
|
|
332
|
-
if (identityType === "passport")
|
|
333
|
-
return "Passport";
|
|
334
|
-
if (identityType === "driver_licence")
|
|
335
|
-
return "Driver's licence";
|
|
336
|
-
return "";
|
|
337
|
-
},
|
|
338
|
-
/**
|
|
339
|
-
* Returns a list of all counties in Kenya.
|
|
340
|
-
* @param {"name" | "code"} sortBy - The field to sort the counties by, i.e. `name` or `code`; defaults to `name`
|
|
341
|
-
* @returns {Promise<County[]>} A list of objects with the county `name`, `code`, and a list of `subCounties`
|
|
342
|
-
*/
|
|
343
|
-
getKenyaCounties: async (sortBy = "name") => {
|
|
344
|
-
// Create Kenya administrative divisions instance
|
|
345
|
-
const kenyaAdmin = new KenyaAdministrativeDivisions();
|
|
346
|
-
// Get all counties
|
|
347
|
-
const countiesData = await kenyaAdmin.getAll();
|
|
348
|
-
// Format counties
|
|
349
|
-
const counties = countiesData.map((county) => {
|
|
350
|
-
// Get list of sub-counties
|
|
351
|
-
const subCounties = [];
|
|
352
|
-
for (const subCounty of county.constituencies)
|
|
353
|
-
subCounties.push(subCounty.constituency_name);
|
|
354
|
-
subCounties.sort();
|
|
355
|
-
// Format county name and add to list
|
|
356
|
-
const countyName = county.county_name.split(" ");
|
|
357
|
-
let name = county.county_name;
|
|
358
|
-
if (countyName.length > 1)
|
|
359
|
-
name = countyName.map((word) => Utility.capitalise(word)).join(" ");
|
|
360
|
-
// Return county
|
|
361
|
-
return { name, code: county.county_code, subCounties };
|
|
362
|
-
});
|
|
363
|
-
// Return sorted counties
|
|
364
|
-
return counties.sort((a, b) => (sortBy === "name" ? a.name.localeCompare(b.name) : a.code - b.code));
|
|
365
|
-
},
|
|
366
|
-
/**
|
|
367
|
-
* Formats a date to an ISO string in Kenyan time, i.e. `Africa/Nairobi` (UTC+3).
|
|
368
|
-
* @param {Date | string | number | null} date - The date to format
|
|
369
|
-
* @returns {string} The formatted date in ISO string format, or an empty string if invalid input
|
|
370
|
-
*/
|
|
371
|
-
getKenyanISOString: (date) => {
|
|
372
|
-
// Return empty string if no date is provided or input is invalid
|
|
373
|
-
if (!date || (typeof date !== "string" && typeof date !== "number" && !(date instanceof Date)))
|
|
374
|
-
return "";
|
|
375
|
-
// Get locale string to format
|
|
376
|
-
const dateObj = date instanceof Date ? date : new Date(date);
|
|
377
|
-
const localeString = dateObj.toLocaleString("sv-SE", {
|
|
378
|
-
timeZone: "Africa/Nairobi"
|
|
379
|
-
});
|
|
380
|
-
// Return formatted string
|
|
381
|
-
return localeString.replace(" ", "T") + "+03:00";
|
|
382
|
-
},
|
|
383
|
-
/**
|
|
384
|
-
* Returns the URL for a menu item based on the slug.
|
|
385
|
-
* @param {string} base - The base URL
|
|
386
|
-
* @param {string} slug - The slug of the menu item
|
|
387
|
-
* @returns {string} The URL for the menu item
|
|
388
|
-
*/
|
|
389
|
-
getMenuItemUrl: (base, slug) => {
|
|
390
|
-
// Validate base URL
|
|
391
|
-
if (typeof base !== "string")
|
|
392
|
-
return "";
|
|
393
|
-
// Format base URL
|
|
394
|
-
if (base === "/")
|
|
395
|
-
base = "";
|
|
396
|
-
if (base.charAt(base.length - 1) === "/")
|
|
397
|
-
base = base.slice(0, -1);
|
|
398
|
-
// Return URL
|
|
399
|
-
return `${base}${slug === "/" ? slug : typeof slug === "string" ? `/${slug}` : ""}`;
|
|
400
|
-
},
|
|
401
|
-
/**
|
|
402
|
-
* Returns the parent URL for a given base URL.
|
|
403
|
-
* @param {string} base - The base URL
|
|
404
|
-
* @returns {string} The parent URL
|
|
405
|
-
*/
|
|
406
|
-
getParentUrl: (base) => {
|
|
407
|
-
// Validate input
|
|
408
|
-
if (typeof base !== "string")
|
|
409
|
-
return "";
|
|
410
|
-
// Return parent URL
|
|
411
|
-
return base.replace(/\/$/, "").split("/").slice(0, -1).join("/") || "/";
|
|
412
|
-
},
|
|
413
|
-
/**
|
|
414
|
-
* Returns a pronoun given pronouns and a type.
|
|
415
|
-
* @param {PronounsItem | null} pronouns - The pronouns to be formatted
|
|
416
|
-
* @param {"subject" | "object" | "possessive" | "reflexive"} type - The type of pronoun to be returned
|
|
417
|
-
* @param {Sex} sex - The user's sex; defaults to `undefined`
|
|
418
|
-
* @returns The formatted pronoun, or `null` if the input is invalid
|
|
419
|
-
*/
|
|
420
|
-
getPronoun: (pronouns, type, sex) => {
|
|
421
|
-
// Get pronoun from pronouns item
|
|
422
|
-
if (pronouns) {
|
|
423
|
-
// Return subject pronoun
|
|
424
|
-
if (type === "subject") {
|
|
425
|
-
if (pronouns === "he/him/his/himself")
|
|
426
|
-
return "he";
|
|
427
|
-
if (pronouns === "she/her/hers/herself")
|
|
428
|
-
return "she";
|
|
429
|
-
if (pronouns === "they/them/their/themself")
|
|
430
|
-
return "they";
|
|
431
|
-
if (pronouns === "name_only")
|
|
432
|
-
return "";
|
|
433
|
-
}
|
|
434
|
-
// Return object pronoun
|
|
435
|
-
else if (type === "object") {
|
|
436
|
-
if (pronouns === "he/him/his/himself")
|
|
437
|
-
return "him";
|
|
438
|
-
if (pronouns === "she/her/hers/herself")
|
|
439
|
-
return "her";
|
|
440
|
-
if (pronouns === "they/them/their/themself")
|
|
441
|
-
return "them";
|
|
442
|
-
if (pronouns === "name_only")
|
|
443
|
-
return "";
|
|
444
|
-
}
|
|
445
|
-
// Return possessive pronoun
|
|
446
|
-
else if (type === "possessive") {
|
|
447
|
-
if (pronouns === "he/him/his/himself")
|
|
448
|
-
return "his";
|
|
449
|
-
if (pronouns === "she/her/hers/herself")
|
|
450
|
-
return "her";
|
|
451
|
-
if (pronouns === "they/them/their/themself")
|
|
452
|
-
return "their";
|
|
453
|
-
if (pronouns === "name_only")
|
|
454
|
-
return "";
|
|
455
|
-
}
|
|
456
|
-
// Return reflexive pronoun
|
|
457
|
-
else if (type === "reflexive") {
|
|
458
|
-
if (pronouns === "he/him/his/himself")
|
|
459
|
-
return "himself";
|
|
460
|
-
if (pronouns === "she/her/hers/herself")
|
|
461
|
-
return "herself";
|
|
462
|
-
if (pronouns === "they/them/their/themself")
|
|
463
|
-
return "themself";
|
|
464
|
-
if (pronouns === "name_only")
|
|
465
|
-
return "";
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
// Assume pronoun for sex if no pronouns are provided
|
|
469
|
-
else if (sex) {
|
|
470
|
-
// Return subject pronoun
|
|
471
|
-
if (type === "subject") {
|
|
472
|
-
if (sex === "male" || sex === "intersex_man")
|
|
473
|
-
return "he";
|
|
474
|
-
if (sex === "female" || sex === "intersex_woman")
|
|
475
|
-
return "she";
|
|
476
|
-
return "they";
|
|
477
|
-
}
|
|
478
|
-
// Return object pronoun
|
|
479
|
-
else if (type === "object") {
|
|
480
|
-
if (sex === "male" || sex === "intersex_man")
|
|
481
|
-
return "him";
|
|
482
|
-
if (sex === "female" || sex === "intersex_woman")
|
|
483
|
-
return "her";
|
|
484
|
-
return "them";
|
|
485
|
-
}
|
|
486
|
-
// Return possessive pronoun
|
|
487
|
-
else if (type === "possessive") {
|
|
488
|
-
if (sex === "male" || sex === "intersex_man")
|
|
489
|
-
return "his";
|
|
490
|
-
if (sex === "female" || sex === "intersex_woman")
|
|
491
|
-
return "her";
|
|
492
|
-
return "their";
|
|
493
|
-
}
|
|
494
|
-
// Return reflexive pronoun
|
|
495
|
-
else if (type === "reflexive") {
|
|
496
|
-
if (sex === "male" || sex === "intersex_man")
|
|
497
|
-
return "himself";
|
|
498
|
-
if (sex === "female" || sex === "intersex_woman")
|
|
499
|
-
return "herself";
|
|
500
|
-
return "themself";
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
// Return null
|
|
504
|
-
return null;
|
|
505
|
-
},
|
|
506
|
-
/**
|
|
507
|
-
* Returns the redirect URL for a given URI.
|
|
508
|
-
* @param {string} uri - The URI to get the redirect URL for, i.e. `microserviceGroup:path` (e.g. `players:fgc/tournaments`)
|
|
509
|
-
* @param {"development" | "production"} env - The environment to use for the redirect URL; defaults to `production`
|
|
510
|
-
* @returns {string} An object with the redirect `url`, or an `error` if any occurs
|
|
511
|
-
*/
|
|
512
|
-
getRedirectUrl: (uri, env = "production") => {
|
|
513
|
-
// Get microservice groups
|
|
514
|
-
const microserviceGroups = ["public", "players", "lounges", "backroom"];
|
|
515
|
-
// Extract microservice group and path from URI
|
|
516
|
-
let [microserviceGroup, path] = uri.split(":");
|
|
517
|
-
if (!microserviceGroup || !path)
|
|
518
|
-
return { error: Utility.customError(400, "URI is invalid.") };
|
|
519
|
-
if (!microserviceGroups.includes(microserviceGroup))
|
|
520
|
-
return { error: Utility.customError(400, "Microservice group is invalid.") };
|
|
521
|
-
// Initialise port number for local development
|
|
522
|
-
let port;
|
|
523
|
-
// Return redirect URL for Public microservices
|
|
524
|
-
if (microserviceGroup === "public") {
|
|
525
|
-
// Initialise subdomain for production
|
|
526
|
-
let subdomain = "";
|
|
527
|
-
// Handle Authentication microservice
|
|
528
|
-
if (path.startsWith("auth")) {
|
|
529
|
-
subdomain = "auth.";
|
|
530
|
-
port = 5173;
|
|
531
|
-
path = path.replace("auth", "");
|
|
532
|
-
}
|
|
533
|
-
// Handle Legal microservice
|
|
534
|
-
else if (path.startsWith("legal")) {
|
|
535
|
-
subdomain = "legal.";
|
|
536
|
-
port = 5174;
|
|
537
|
-
path = path.replace("legal", "");
|
|
538
|
-
}
|
|
539
|
-
// Handle Landing microservice
|
|
540
|
-
else if (path.startsWith("landing")) {
|
|
541
|
-
port = 5184;
|
|
542
|
-
path = path.replace("landing", "");
|
|
543
|
-
}
|
|
544
|
-
// Return redirect URL
|
|
545
|
-
return {
|
|
546
|
-
url: env === "production"
|
|
547
|
-
? `https://${subdomain}koloseum.ke${path || ""}`
|
|
548
|
-
: `http://127.0.0.1:${port}${path || ""}`
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
// Handle Players microservices
|
|
552
|
-
if (microserviceGroup === "players") {
|
|
553
|
-
if (path.startsWith("account")) {
|
|
554
|
-
port = 5175;
|
|
555
|
-
if (env === "development")
|
|
556
|
-
path = path.replace("account", "");
|
|
557
|
-
}
|
|
558
|
-
if (path.startsWith("fgc")) {
|
|
559
|
-
port = 5178;
|
|
560
|
-
if (env === "development")
|
|
561
|
-
path = path.replace("fgc", "");
|
|
562
|
-
}
|
|
563
|
-
if (path.startsWith("commerce")) {
|
|
564
|
-
port = 5179;
|
|
565
|
-
if (env === "development")
|
|
566
|
-
path = path.replace("commerce", "");
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
// Handle Lounges microservices
|
|
570
|
-
if (microserviceGroup === "lounges") {
|
|
571
|
-
if (path.startsWith("branches")) {
|
|
572
|
-
port = 5181;
|
|
573
|
-
if (env === "development")
|
|
574
|
-
path = path.replace("branches", "");
|
|
575
|
-
}
|
|
576
|
-
if (path.startsWith("staff")) {
|
|
577
|
-
port = 5182;
|
|
578
|
-
if (env === "development")
|
|
579
|
-
path = path.replace("staff", "");
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
// Handle Backroom microservices
|
|
583
|
-
if (microserviceGroup === "backroom") {
|
|
584
|
-
if (path.startsWith("compliance")) {
|
|
585
|
-
port = 5176;
|
|
586
|
-
if (env === "development")
|
|
587
|
-
path = path.replace("compliance", "");
|
|
588
|
-
}
|
|
589
|
-
if (path.startsWith("competitions")) {
|
|
590
|
-
port = 5177;
|
|
591
|
-
if (env === "development")
|
|
592
|
-
path = path.replace("competitions", "");
|
|
593
|
-
}
|
|
594
|
-
if (path.startsWith("commerce")) {
|
|
595
|
-
port = 5180;
|
|
596
|
-
if (env === "development")
|
|
597
|
-
path = path.replace("commerce", "");
|
|
598
|
-
}
|
|
599
|
-
if (path.startsWith("staff")) {
|
|
600
|
-
port = 5183;
|
|
601
|
-
if (env === "development")
|
|
602
|
-
path = path.replace("staff", "");
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
// Return redirect URL
|
|
606
|
-
return {
|
|
607
|
-
url: env === "production"
|
|
608
|
-
? `https://${microserviceGroup}.koloseum.ke/${path || ""}`
|
|
609
|
-
: `http://127.0.0.1:${port}${path || ""}`
|
|
610
|
-
};
|
|
611
|
-
},
|
|
612
|
-
/**
|
|
613
|
-
* Generate a SuprSend notification inbox configuration object for a user.
|
|
614
|
-
* @param {string} userId - The user ID to generate the configuration for.
|
|
615
|
-
* @param {string} publicApiKey - The public API key to use for SuprSend.
|
|
616
|
-
* @returns The SuprSend notification inbox configuration object.
|
|
617
|
-
*/
|
|
618
|
-
getSuprSendInboxConfig: (userId, publicApiKey) => ({
|
|
619
|
-
distinctId: userId,
|
|
620
|
-
publicApiKey,
|
|
621
|
-
inbox: {
|
|
622
|
-
stores: [
|
|
623
|
-
{ storeId: "all", label: "Inbox", query: { archived: false } },
|
|
624
|
-
{ storeId: "archived", label: "Archived", query: { archived: true } }
|
|
625
|
-
],
|
|
626
|
-
theme: {
|
|
627
|
-
bell: {
|
|
628
|
-
color: "var(--color-accent)"
|
|
629
|
-
},
|
|
630
|
-
badge: {
|
|
631
|
-
backgroundColor: "var(--color-primary)",
|
|
632
|
-
color: "var(--color-primary-content)"
|
|
633
|
-
},
|
|
634
|
-
header: {
|
|
635
|
-
container: {
|
|
636
|
-
backgroundColor: "var(--color-base-100)",
|
|
637
|
-
borderColor: "var(--color-base-200)"
|
|
638
|
-
},
|
|
639
|
-
headerText: {
|
|
640
|
-
color: "var(--color-neutral)"
|
|
641
|
-
},
|
|
642
|
-
markAllReadText: {
|
|
643
|
-
color: "var(--color-accent)"
|
|
644
|
-
}
|
|
645
|
-
},
|
|
646
|
-
tabs: {
|
|
647
|
-
color: "var(--color-primary)",
|
|
648
|
-
unselectedColor: "var(--color-accent)",
|
|
649
|
-
bottomColor: "var(--color-primary)",
|
|
650
|
-
badgeColor: "var(--color-base-200)",
|
|
651
|
-
badgeText: "var(--color-accent)"
|
|
652
|
-
},
|
|
653
|
-
notificationsContainer: {
|
|
654
|
-
container: {
|
|
655
|
-
backgroundColor: "var(--color-base-100)",
|
|
656
|
-
borderColor: "var(--color-base-200)",
|
|
657
|
-
height: "75vh",
|
|
658
|
-
marginTop: "0.75rem"
|
|
659
|
-
},
|
|
660
|
-
noNotificationsText: {
|
|
661
|
-
color: "var(--color-warning)"
|
|
662
|
-
},
|
|
663
|
-
noNotificationsSubtext: {
|
|
664
|
-
color: "var(--color-neutral)"
|
|
665
|
-
},
|
|
666
|
-
loader: {
|
|
667
|
-
color: "var(--color-primary)"
|
|
668
|
-
}
|
|
669
|
-
},
|
|
670
|
-
notification: {
|
|
671
|
-
container: {
|
|
672
|
-
borderColor: "var(--color-base-200)",
|
|
673
|
-
readBackgroundColor: "var(--color-base-100)",
|
|
674
|
-
unreadBackgroundColor: "var(--color-base-150)",
|
|
675
|
-
hoverBackgroundColor: "var(--color-base-200)"
|
|
676
|
-
},
|
|
677
|
-
headerText: {
|
|
678
|
-
color: "var(--color-secondary)"
|
|
679
|
-
},
|
|
680
|
-
bodyText: {
|
|
681
|
-
color: "var(--color-neutral)",
|
|
682
|
-
linkColor: "var(--color-secondary)"
|
|
683
|
-
},
|
|
684
|
-
unseenDot: {
|
|
685
|
-
backgroundColor: "var(--color-warning)"
|
|
686
|
-
},
|
|
687
|
-
createdOnText: {
|
|
688
|
-
color: "var(--color-accent)"
|
|
689
|
-
},
|
|
690
|
-
subtext: {
|
|
691
|
-
color: "var(--color-neutral)"
|
|
692
|
-
},
|
|
693
|
-
expiresText: {
|
|
694
|
-
color: "var(--color-warning)",
|
|
695
|
-
expiringBackgroundColor: "var(--color-warning)",
|
|
696
|
-
expiringColor: "var(--color-neutral)"
|
|
697
|
-
},
|
|
698
|
-
actions: [
|
|
699
|
-
{
|
|
700
|
-
text: {
|
|
701
|
-
color: "var(--color-primary-content)"
|
|
702
|
-
},
|
|
703
|
-
container: {
|
|
704
|
-
backgroundColor: "var(--color-primary)",
|
|
705
|
-
hoverBackgroundColor: "var(--color-secondary)",
|
|
706
|
-
border: "unset"
|
|
707
|
-
}
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
text: {
|
|
711
|
-
color: "var(--color-primary-content)"
|
|
712
|
-
},
|
|
713
|
-
container: {
|
|
714
|
-
backgroundColor: "var(--color-primary)",
|
|
715
|
-
hoverBackgroundColor: "var(--color-secondary)",
|
|
716
|
-
border: "unset"
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
],
|
|
720
|
-
actionsMenuIcon: {
|
|
721
|
-
hoverBackgroundColor: "var(--color-base-200)",
|
|
722
|
-
color: "var(--color-neutral)"
|
|
723
|
-
},
|
|
724
|
-
actionsMenu: {
|
|
725
|
-
backgroundColor: "var(--color-base-100)",
|
|
726
|
-
borderColor: "var(--color-base-200)"
|
|
727
|
-
},
|
|
728
|
-
actionsMenuItem: {
|
|
729
|
-
hoverBackgroundColor: "var(--color-base-200)"
|
|
730
|
-
},
|
|
731
|
-
actionsMenuItemIcon: {
|
|
732
|
-
color: "var(--color-accent)"
|
|
733
|
-
},
|
|
734
|
-
actionsMenuItemText: {
|
|
735
|
-
color: "var(--color-neutral)"
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
},
|
|
740
|
-
toast: {
|
|
741
|
-
theme: {
|
|
742
|
-
container: {
|
|
743
|
-
backgroundColor: "var(--color-base-100)",
|
|
744
|
-
borderColor: "var(--color-base-200)"
|
|
745
|
-
},
|
|
746
|
-
headerText: {
|
|
747
|
-
color: "var(--color-primary)"
|
|
748
|
-
},
|
|
749
|
-
bodyText: {
|
|
750
|
-
color: "var(--color-neutral)"
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}),
|
|
755
|
-
/**
|
|
756
|
-
* Handles the click event for pronouns checkboxes.
|
|
757
|
-
* @param {MouseEvent} e - The click event
|
|
758
|
-
* @param {PronounsCheckboxes} checkboxes - The pronouns checkboxes
|
|
759
|
-
*/
|
|
760
|
-
handlePronounsCheckboxClick: (e, checkboxes) => {
|
|
761
|
-
const target = e.target;
|
|
762
|
-
if (target.value === "none") {
|
|
763
|
-
for (let checkbox of Object.values(checkboxes))
|
|
764
|
-
if (checkbox && checkbox !== target && checkbox.checked) {
|
|
765
|
-
e.preventDefault();
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
for (let checkbox of Object.values(checkboxes))
|
|
769
|
-
if (checkbox && checkbox !== target) {
|
|
770
|
-
checkbox.checked = false;
|
|
771
|
-
checkbox.disabled = !checkbox.disabled;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
else {
|
|
775
|
-
if (checkboxes.none?.checked) {
|
|
776
|
-
e.preventDefault();
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
if (target.value === "male" || target.value === "female") {
|
|
780
|
-
const oppositeIndex = target.value === "male" ? "female" : "male";
|
|
781
|
-
const oppositeCheckbox = checkboxes[oppositeIndex];
|
|
782
|
-
if (oppositeCheckbox?.checked) {
|
|
783
|
-
e.preventDefault();
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
if (oppositeCheckbox) {
|
|
787
|
-
oppositeCheckbox.checked = false;
|
|
788
|
-
oppositeCheckbox.disabled = !oppositeCheckbox.disabled;
|
|
789
|
-
}
|
|
790
|
-
if (checkboxes.none) {
|
|
791
|
-
checkboxes.none.checked = false;
|
|
792
|
-
if (!checkboxes.neutral?.checked)
|
|
793
|
-
checkboxes.none.disabled = !checkboxes.none.disabled;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
else {
|
|
797
|
-
if (checkboxes.none) {
|
|
798
|
-
checkboxes.none.checked = false;
|
|
799
|
-
if (checkboxes.neutral && !checkboxes.male?.checked && !checkboxes.female?.checked)
|
|
800
|
-
checkboxes.none.disabled = checkboxes.neutral.checked;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
},
|
|
805
|
-
/**
|
|
806
|
-
* Checks if a user is authorised to access a microservice.
|
|
807
|
-
* @param {SupabaseClient<Database>} supabase - The Supabase client.
|
|
808
|
-
* @param {UserWithCustomMetadata} user - The user to check.
|
|
809
|
-
* @param {Object} options - The options for the function.
|
|
810
|
-
* @param {MicroserviceGroup} options.microserviceGroup - The microservice group.
|
|
811
|
-
* @param {Microservice<MicroserviceGroup> | string} options.microservice - The microservice.
|
|
812
|
-
* @param {string} options.playersUrl - The URL to the Players microservice.
|
|
813
|
-
* @param {(role: string) => string} options.requestedPermission - A function that returns the requested permission for a given role.
|
|
814
|
-
* @returns {Promise<{ isAuthorised?: boolean; redirect?: { code: number; url: string }; error?: CustomError }>} The result of the function.
|
|
815
|
-
*/
|
|
816
|
-
isUserAuthorised: async (supabase, user, options) => {
|
|
817
|
-
// Destructure options
|
|
818
|
-
const { microserviceGroup, microservice, playersUrl } = options;
|
|
819
|
-
// Return true if microservice group is public
|
|
820
|
-
if (microserviceGroup === "public")
|
|
821
|
-
return { isAuthorised: true };
|
|
822
|
-
// Validate user metadata and app metadata
|
|
823
|
-
if (!user.user_metadata || !user.app_metadata)
|
|
824
|
-
return { error: Utility.customError(400, "User metadata is required.") };
|
|
825
|
-
// Get user's roles and the role prefix
|
|
826
|
-
const roles = [];
|
|
827
|
-
const rolePrefix = microserviceGroup === "backroom" ? "backroom" : microserviceGroup.slice(0, -1);
|
|
828
|
-
for (const role of user.app_metadata.roles) {
|
|
829
|
-
if (role === "player") {
|
|
830
|
-
if (microserviceGroup === "players")
|
|
831
|
-
roles.push(role);
|
|
832
|
-
else
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
if (role.startsWith(`${rolePrefix}_`))
|
|
836
|
-
roles.push(role.replace(`${rolePrefix}_`, ""));
|
|
837
|
-
}
|
|
838
|
-
// Redirect to Players microservices if user does not have any roles for the microservice group
|
|
839
|
-
if (roles.length === 0 && microserviceGroup !== "players") {
|
|
840
|
-
// Return error if Players URL is not provided
|
|
841
|
-
if (!playersUrl)
|
|
842
|
-
return { error: Utility.customError(400, "Players URL is required.") };
|
|
843
|
-
// Redirect to Players microservices
|
|
844
|
-
return { redirect: { code: 307, url: playersUrl } };
|
|
845
|
-
}
|
|
846
|
-
// Destructure role
|
|
847
|
-
const [role] = roles;
|
|
848
|
-
// Define condition variables
|
|
849
|
-
const isPlayer = microserviceGroup === "players" && role === "player";
|
|
850
|
-
const isSuperuser = (microserviceGroup === "backroom" || microserviceGroup === "lounges") && role === "superuser";
|
|
851
|
-
let isAuthorised = false;
|
|
852
|
-
// Grant access if Player (accessing Players microservices), superuser, or account microservice
|
|
853
|
-
if (isPlayer || isSuperuser || microservice === "account")
|
|
854
|
-
isAuthorised = true;
|
|
855
|
-
// Evaluate access
|
|
856
|
-
else if (microserviceGroup !== "players") {
|
|
857
|
-
// Return error if requested permission is not provided
|
|
858
|
-
if (!options.requestedPermission)
|
|
859
|
-
return { error: Utility.customError(400, "Requested permission is required.") };
|
|
860
|
-
// Get user's role and requested app permission
|
|
861
|
-
const requestedPermission = options.requestedPermission(role);
|
|
862
|
-
// Check if user has the requested permission
|
|
863
|
-
const { data, error: isAuthorisedError } = await supabase
|
|
864
|
-
.schema("compliance")
|
|
865
|
-
.rpc("authorise", { requested_permission: requestedPermission });
|
|
866
|
-
if (isAuthorisedError)
|
|
867
|
-
return Utility.parsePostgrestError(isAuthorisedError);
|
|
868
|
-
// Assign result of authorisation check
|
|
869
|
-
isAuthorised = data;
|
|
870
|
-
}
|
|
871
|
-
// If user has completed Player registration
|
|
872
|
-
if (roles.includes("player")) {
|
|
873
|
-
// Prepare person data
|
|
874
|
-
let personData = {};
|
|
875
|
-
if (user.app_metadata.person_data) {
|
|
876
|
-
const { first_name: firstName, last_name: lastName, pseudonym } = user.app_metadata.person_data;
|
|
877
|
-
personData = { firstName, lastName, phone: user.phone, pseudonym };
|
|
878
|
-
}
|
|
879
|
-
// Validate SuprSend subscriber
|
|
880
|
-
const { code, error: validationError } = await Utility.callEdgeFunction(false, supabase, `suprsend/validate-subscriber?user-id=${user.id}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: personData });
|
|
881
|
-
if (validationError)
|
|
882
|
-
return { error: Utility.customError(code, validationError) };
|
|
883
|
-
// Send welcome notification if not already sent
|
|
884
|
-
const { error: welcomeError } = await Utility.sendWelcomeNotification(supabase, user, microserviceGroup);
|
|
885
|
-
if (welcomeError)
|
|
886
|
-
return { error: welcomeError };
|
|
887
|
-
}
|
|
888
|
-
// Return result
|
|
889
|
-
return { isAuthorised };
|
|
890
|
-
},
|
|
891
|
-
/**
|
|
892
|
-
* A reference of microservices available on the platform, represented as an object with each user group (i.e. `public`, `players`, `lounges`, and `backroom`) having its own array of microservices. Each microservice in a list is an object with the following properties:
|
|
893
|
-
* - `name`: The name of the microservice.
|
|
894
|
-
* - `description`: A description of the microservice.
|
|
895
|
-
* - `slug`: The slug of the microservice.
|
|
896
|
-
* - `features`: An array of features that the microservice has.
|
|
897
|
-
* - `roles`: An array of roles attached to the microservice.
|
|
898
|
-
*/
|
|
899
|
-
microservices: {
|
|
900
|
-
backroom: [
|
|
901
|
-
{
|
|
902
|
-
name: "Compliance",
|
|
903
|
-
description: "Management of compliance and regulatory adherence for Players and Lounges.",
|
|
904
|
-
slug: "compliance",
|
|
905
|
-
features: [
|
|
906
|
-
{
|
|
907
|
-
name: "Players",
|
|
908
|
-
description: "Search, review, and manage Player data.",
|
|
909
|
-
slug: "players",
|
|
910
|
-
tabs: [
|
|
911
|
-
{
|
|
912
|
-
name: "Profile",
|
|
913
|
-
description: "Review basic Player data.",
|
|
914
|
-
root: true
|
|
915
|
-
},
|
|
916
|
-
{
|
|
917
|
-
name: "Documents",
|
|
918
|
-
description: "Review Player documents.",
|
|
919
|
-
slug: "documents"
|
|
920
|
-
},
|
|
921
|
-
{
|
|
922
|
-
name: "Data updates",
|
|
923
|
-
description: "Review Player data updates.",
|
|
924
|
-
slug: "data-updates",
|
|
925
|
-
tabs: [
|
|
926
|
-
{
|
|
927
|
-
name: "Requests",
|
|
928
|
-
description: "Review Player data update requests.",
|
|
929
|
-
root: true
|
|
930
|
-
},
|
|
931
|
-
{
|
|
932
|
-
name: "History",
|
|
933
|
-
description: "Review Player data update history.",
|
|
934
|
-
slug: "history"
|
|
935
|
-
}
|
|
936
|
-
]
|
|
937
|
-
},
|
|
938
|
-
{
|
|
939
|
-
name: "Credits",
|
|
940
|
-
description: "Review Koloseum Credits data for the Player.",
|
|
941
|
-
slug: "credits",
|
|
942
|
-
tabs: [
|
|
943
|
-
{
|
|
944
|
-
name: "Transactions",
|
|
945
|
-
description: "Review Player transactions with Koloseum Credits.",
|
|
946
|
-
root: true
|
|
947
|
-
},
|
|
948
|
-
{
|
|
949
|
-
name: "Transfers",
|
|
950
|
-
description: "Review Player transfers with Koloseum Credits.",
|
|
951
|
-
slug: "transfers"
|
|
952
|
-
},
|
|
953
|
-
{
|
|
954
|
-
name: "Subscriptions",
|
|
955
|
-
description: "Review Player subscriptions with Koloseum Credits.",
|
|
956
|
-
slug: "subscriptions"
|
|
957
|
-
}
|
|
958
|
-
]
|
|
959
|
-
},
|
|
960
|
-
{
|
|
961
|
-
name: "Misconduct",
|
|
962
|
-
description: "Review Player reports and sanctions.",
|
|
963
|
-
slug: "misconduct",
|
|
964
|
-
tabs: [
|
|
965
|
-
{
|
|
966
|
-
name: "Reports",
|
|
967
|
-
description: "Review Player reports.",
|
|
968
|
-
root: true
|
|
969
|
-
},
|
|
970
|
-
{
|
|
971
|
-
name: "Sanctions",
|
|
972
|
-
description: "Review Player sanctions.",
|
|
973
|
-
slug: "sanctions"
|
|
974
|
-
}
|
|
975
|
-
]
|
|
976
|
-
},
|
|
977
|
-
{
|
|
978
|
-
name: "Points / KXP",
|
|
979
|
-
description: "Review Koloseum Experience Points (KXP) transactions for the Player.",
|
|
980
|
-
slug: "points"
|
|
981
|
-
}
|
|
982
|
-
]
|
|
983
|
-
},
|
|
984
|
-
{
|
|
985
|
-
name: "Lounges",
|
|
986
|
-
description: "Search, review, and manage Lounge data.",
|
|
987
|
-
slug: "lounges",
|
|
988
|
-
tabs: [
|
|
989
|
-
{
|
|
990
|
-
name: "Profile",
|
|
991
|
-
description: "Review basic Lounge data.",
|
|
992
|
-
root: true
|
|
993
|
-
},
|
|
994
|
-
{
|
|
995
|
-
name: "Documents",
|
|
996
|
-
description: "Review Lounge documents.",
|
|
997
|
-
slug: "documents"
|
|
998
|
-
},
|
|
999
|
-
{
|
|
1000
|
-
name: "Staff",
|
|
1001
|
-
description: "Search, review, and manage Lounge staff data.",
|
|
1002
|
-
slug: "staff"
|
|
1003
|
-
},
|
|
1004
|
-
{
|
|
1005
|
-
name: "Branches",
|
|
1006
|
-
description: "Review Lounge branches.",
|
|
1007
|
-
slug: "branches",
|
|
1008
|
-
tabs: [
|
|
1009
|
-
{
|
|
1010
|
-
name: "Profile",
|
|
1011
|
-
description: "Review basic Lounge branch data.",
|
|
1012
|
-
root: true
|
|
1013
|
-
},
|
|
1014
|
-
{
|
|
1015
|
-
name: "Amenities",
|
|
1016
|
-
description: "Review Lounge branch amenities.",
|
|
1017
|
-
slug: "amenities"
|
|
1018
|
-
},
|
|
1019
|
-
{
|
|
1020
|
-
name: "Staff",
|
|
1021
|
-
description: "Search, review, and manage Lounge branchstaff data.",
|
|
1022
|
-
slug: "staff"
|
|
1023
|
-
}
|
|
1024
|
-
]
|
|
1025
|
-
},
|
|
1026
|
-
{
|
|
1027
|
-
name: "Data updates",
|
|
1028
|
-
description: "Review Lounge data updates.",
|
|
1029
|
-
slug: "data-updates",
|
|
1030
|
-
tabs: [
|
|
1031
|
-
{
|
|
1032
|
-
name: "Requests",
|
|
1033
|
-
description: "Review Lounge data update requests.",
|
|
1034
|
-
root: true
|
|
1035
|
-
},
|
|
1036
|
-
{
|
|
1037
|
-
name: "History",
|
|
1038
|
-
description: "Review Lounge data update history.",
|
|
1039
|
-
slug: "history"
|
|
1040
|
-
}
|
|
1041
|
-
]
|
|
1042
|
-
},
|
|
1043
|
-
{
|
|
1044
|
-
name: "Credits",
|
|
1045
|
-
description: "Review Koloseum Credits data for the Lounge.",
|
|
1046
|
-
slug: "credits",
|
|
1047
|
-
tabs: [
|
|
1048
|
-
{
|
|
1049
|
-
name: "Transactions",
|
|
1050
|
-
description: "Review Lounge transactions with Koloseum Credits.",
|
|
1051
|
-
root: true
|
|
1052
|
-
},
|
|
1053
|
-
{
|
|
1054
|
-
name: "Transfers",
|
|
1055
|
-
description: "Review Lounge transfers with Koloseum Credits.",
|
|
1056
|
-
slug: "transfers"
|
|
1057
|
-
},
|
|
1058
|
-
{
|
|
1059
|
-
name: "Subscriptions",
|
|
1060
|
-
description: "Review Lounge subscriptions with Koloseum Credits.",
|
|
1061
|
-
slug: "subscriptions"
|
|
1062
|
-
}
|
|
1063
|
-
]
|
|
1064
|
-
},
|
|
1065
|
-
{
|
|
1066
|
-
name: "Misconduct",
|
|
1067
|
-
description: "Review Lounge reports and sanctions.",
|
|
1068
|
-
slug: "misconduct",
|
|
1069
|
-
tabs: [
|
|
1070
|
-
{
|
|
1071
|
-
name: "Reports",
|
|
1072
|
-
description: "Review Lounge reports.",
|
|
1073
|
-
root: true
|
|
1074
|
-
},
|
|
1075
|
-
{
|
|
1076
|
-
name: "Sanctions",
|
|
1077
|
-
description: "Review Lounge sanctions.",
|
|
1078
|
-
slug: "sanctions"
|
|
1079
|
-
}
|
|
1080
|
-
]
|
|
1081
|
-
},
|
|
1082
|
-
{
|
|
1083
|
-
name: "Points / KXP",
|
|
1084
|
-
description: "Review Koloseum Experience Points (KXP) transactions for the Lounge.",
|
|
1085
|
-
slug: "points"
|
|
1086
|
-
}
|
|
1087
|
-
]
|
|
1088
|
-
}
|
|
1089
|
-
],
|
|
1090
|
-
roles: [
|
|
1091
|
-
{
|
|
1092
|
-
name: "Compliance",
|
|
1093
|
-
slug: "compliance",
|
|
1094
|
-
root: true
|
|
1095
|
-
},
|
|
1096
|
-
{
|
|
1097
|
-
name: "Players",
|
|
1098
|
-
slug: "players",
|
|
1099
|
-
featureSlugs: ["players"]
|
|
1100
|
-
},
|
|
1101
|
-
{
|
|
1102
|
-
name: "Lounges",
|
|
1103
|
-
slug: "lounges",
|
|
1104
|
-
featureSlugs: ["lounges"]
|
|
1105
|
-
}
|
|
1106
|
-
]
|
|
1107
|
-
},
|
|
1108
|
-
{
|
|
1109
|
-
name: "Competitions",
|
|
1110
|
-
description: "Management of competitions data and live events across Markets.",
|
|
1111
|
-
slug: "competitions",
|
|
1112
|
-
features: [
|
|
1113
|
-
{
|
|
1114
|
-
name: "Ma Esto",
|
|
1115
|
-
description: "Manage football esports data for Ma Esto.",
|
|
1116
|
-
slug: "fbl"
|
|
1117
|
-
},
|
|
1118
|
-
{
|
|
1119
|
-
name: "Savanna FGC",
|
|
1120
|
-
description: "Manage fighting games esports data for Savanna FGC.",
|
|
1121
|
-
slug: "fgc",
|
|
1122
|
-
tabs: [
|
|
1123
|
-
{
|
|
1124
|
-
name: "Savanna Circuit",
|
|
1125
|
-
description: "Manage competition data for the Savanna Circuit.",
|
|
1126
|
-
slug: "league",
|
|
1127
|
-
tabs: [
|
|
1128
|
-
{
|
|
1129
|
-
name: "Rankings",
|
|
1130
|
-
description: "Manage rankings for any Savanna Circuit season.",
|
|
1131
|
-
root: true
|
|
1132
|
-
},
|
|
1133
|
-
{
|
|
1134
|
-
name: "Tournaments",
|
|
1135
|
-
description: "Manage tournaments for any Savanna Circuit season.",
|
|
1136
|
-
slug: "tournaments",
|
|
1137
|
-
tabs: [
|
|
1138
|
-
{
|
|
1139
|
-
name: "Events",
|
|
1140
|
-
description: "Manage events for any Savanna Circuit tournament.",
|
|
1141
|
-
root: true
|
|
1142
|
-
},
|
|
1143
|
-
{
|
|
1144
|
-
name: "Tickets",
|
|
1145
|
-
description: "Manage tickets for any Savanna Circuit tournament.",
|
|
1146
|
-
slug: "tickets"
|
|
1147
|
-
},
|
|
1148
|
-
{
|
|
1149
|
-
name: "Settings",
|
|
1150
|
-
description: "Manage settings for any Savanna Circuit tournament.",
|
|
1151
|
-
slug: "settings"
|
|
1152
|
-
}
|
|
1153
|
-
]
|
|
1154
|
-
},
|
|
1155
|
-
{
|
|
1156
|
-
name: "Settings",
|
|
1157
|
-
description: "Manage settings for any Savanna Circuit season.",
|
|
1158
|
-
slug: "settings"
|
|
1159
|
-
}
|
|
1160
|
-
]
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
name: "Savanna Fight Night",
|
|
1164
|
-
description: "Manage competition data for Savanna Fight Night.",
|
|
1165
|
-
slug: "challenges"
|
|
1166
|
-
}
|
|
1167
|
-
]
|
|
1168
|
-
},
|
|
1169
|
-
{
|
|
1170
|
-
name: "Hit List",
|
|
1171
|
-
description: "Manage battle royale esports data for Hit List.",
|
|
1172
|
-
slug: "bryl"
|
|
1173
|
-
}
|
|
1174
|
-
],
|
|
1175
|
-
roles: [
|
|
1176
|
-
{
|
|
1177
|
-
name: "Competitions",
|
|
1178
|
-
slug: "competitions",
|
|
1179
|
-
root: true
|
|
1180
|
-
},
|
|
1181
|
-
{
|
|
1182
|
-
name: "Ma Esto",
|
|
1183
|
-
slug: "fbl",
|
|
1184
|
-
featureSlugs: ["fbl"]
|
|
1185
|
-
},
|
|
1186
|
-
{
|
|
1187
|
-
name: "Savanna FGC",
|
|
1188
|
-
slug: "fgc",
|
|
1189
|
-
featureSlugs: ["fgc"]
|
|
1190
|
-
},
|
|
1191
|
-
{
|
|
1192
|
-
name: "Hit List",
|
|
1193
|
-
slug: "bryl",
|
|
1194
|
-
featureSlugs: ["bryl"]
|
|
1195
|
-
}
|
|
1196
|
-
]
|
|
1197
|
-
},
|
|
1198
|
-
{
|
|
1199
|
-
name: "KLSM",
|
|
1200
|
-
description: "Management of game and console trades between users.",
|
|
1201
|
-
slug: "commerce",
|
|
1202
|
-
features: [],
|
|
1203
|
-
roles: [
|
|
1204
|
-
{
|
|
1205
|
-
name: "Commerce",
|
|
1206
|
-
slug: "commerce",
|
|
1207
|
-
root: true
|
|
1208
|
-
}
|
|
1209
|
-
]
|
|
1210
|
-
},
|
|
1211
|
-
{
|
|
1212
|
-
name: "Staff",
|
|
1213
|
-
description: "Management of Backroom staff authorisation.",
|
|
1214
|
-
slug: "staff",
|
|
1215
|
-
features: [
|
|
1216
|
-
{
|
|
1217
|
-
name: "Roles",
|
|
1218
|
-
description: "Review and manage Backroom roles.",
|
|
1219
|
-
slug: "roles"
|
|
1220
|
-
},
|
|
1221
|
-
{
|
|
1222
|
-
name: "Users",
|
|
1223
|
-
description: "Review and manage Backroom users.",
|
|
1224
|
-
slug: "users"
|
|
1225
|
-
}
|
|
1226
|
-
],
|
|
1227
|
-
roles: [
|
|
1228
|
-
{
|
|
1229
|
-
name: "Human Resources",
|
|
1230
|
-
slug: "hr",
|
|
1231
|
-
root: true
|
|
1232
|
-
}
|
|
1233
|
-
]
|
|
1234
|
-
}
|
|
1235
|
-
],
|
|
1236
|
-
players: [
|
|
1237
|
-
{
|
|
1238
|
-
name: "Sessions",
|
|
1239
|
-
description: "Track your gaming sessions at registered lounges.",
|
|
1240
|
-
slug: "sessions",
|
|
1241
|
-
features: [],
|
|
1242
|
-
roles: null
|
|
1243
|
-
},
|
|
1244
|
-
{
|
|
1245
|
-
name: "KLSM",
|
|
1246
|
-
description: "Trade used games and consoles with other users at affordable prices.",
|
|
1247
|
-
slug: "commerce",
|
|
1248
|
-
features: [],
|
|
1249
|
-
roles: null
|
|
1250
|
-
},
|
|
1251
|
-
{
|
|
1252
|
-
name: "Savanna FGC",
|
|
1253
|
-
description: "Check out the latest news and competitions from Savanna FGC.",
|
|
1254
|
-
slug: "fgc",
|
|
1255
|
-
features: [
|
|
1256
|
-
{
|
|
1257
|
-
name: "Home",
|
|
1258
|
-
description: "Learn more about the Savanna Circuit.",
|
|
1259
|
-
root: true
|
|
1260
|
-
},
|
|
1261
|
-
{
|
|
1262
|
-
name: "Rules",
|
|
1263
|
-
description: "Review the rules and regulations of the Savanna Circuit.",
|
|
1264
|
-
slug: "rules"
|
|
1265
|
-
},
|
|
1266
|
-
{
|
|
1267
|
-
name: "Tournaments",
|
|
1268
|
-
description: "Review and join upcoming tournaments on the Savanna Circuit.",
|
|
1269
|
-
slug: "tournaments"
|
|
1270
|
-
}
|
|
1271
|
-
],
|
|
1272
|
-
roles: null
|
|
1273
|
-
},
|
|
1274
|
-
{
|
|
1275
|
-
name: "Account",
|
|
1276
|
-
description: "Review Player account settings and preferences.",
|
|
1277
|
-
slug: "account",
|
|
1278
|
-
features: [
|
|
1279
|
-
{
|
|
1280
|
-
name: "Personal",
|
|
1281
|
-
description: "Review and manage your personal information.",
|
|
1282
|
-
slug: "personal"
|
|
1283
|
-
},
|
|
1284
|
-
{
|
|
1285
|
-
name: "Gaming & Socials",
|
|
1286
|
-
description: "Review and manage your gaming and social media information.",
|
|
1287
|
-
slug: "gaming-socials"
|
|
1288
|
-
},
|
|
1289
|
-
{
|
|
1290
|
-
name: "Notifications",
|
|
1291
|
-
description: "Review and manage your notification preferences.",
|
|
1292
|
-
slug: "notifications"
|
|
1293
|
-
},
|
|
1294
|
-
{
|
|
1295
|
-
name: "Security",
|
|
1296
|
-
description: "Review and manage your security preferences.",
|
|
1297
|
-
slug: "security"
|
|
1298
|
-
},
|
|
1299
|
-
{
|
|
1300
|
-
name: "Lounges",
|
|
1301
|
-
description: "Review your access to Lounges microservices.",
|
|
1302
|
-
slug: "lounges"
|
|
1303
|
-
},
|
|
1304
|
-
{
|
|
1305
|
-
name: "Backroom",
|
|
1306
|
-
description: "Review your access to Backroom microservices.",
|
|
1307
|
-
slug: "backroom"
|
|
1308
|
-
}
|
|
1309
|
-
],
|
|
1310
|
-
roles: null
|
|
1311
|
-
}
|
|
1312
|
-
]
|
|
1313
|
-
},
|
|
1314
|
-
/**
|
|
1315
|
-
* The minimum birth date (from today's date) for a user who is a minor, i.e. `YYYY-MM-DD`
|
|
1316
|
-
*/
|
|
1317
|
-
minimumMinorBirthDate: (() => {
|
|
1318
|
-
const today = new Date();
|
|
1319
|
-
const tomorrow = new Date();
|
|
1320
|
-
tomorrow.setUTCDate(today.getDate() + 1);
|
|
1321
|
-
const tomorrow18YearsAgo = tomorrow.getFullYear() - 18;
|
|
1322
|
-
tomorrow.setFullYear(tomorrow18YearsAgo);
|
|
1323
|
-
return tomorrow.toISOString().split("T")[0];
|
|
1324
|
-
})(),
|
|
1325
|
-
/**
|
|
1326
|
-
* Check if a page is active based on the slug and microservice.
|
|
1327
|
-
* @param page - The current page object.
|
|
1328
|
-
* @param slug - The slug of the page to check.
|
|
1329
|
-
* @param microservice - The microservice to check against.
|
|
1330
|
-
* @param base - The base path of the application; defaults to an empty string.
|
|
1331
|
-
* @returns `true` if the page is active, `false` otherwise.
|
|
1332
|
-
*/
|
|
1333
|
-
pageIsActive: (page, slug, microservice, base = "") => {
|
|
1334
|
-
// Return true if microservice is provided and matches slug
|
|
1335
|
-
if (microservice)
|
|
1336
|
-
return slug === microservice;
|
|
1337
|
-
// Remove trailing slash from base for consistency
|
|
1338
|
-
const cleanBase = base.replace(/\/$/, "");
|
|
1339
|
-
// Match exactly the base path if slug is falsy (i.e. root microservice feature)
|
|
1340
|
-
if (!slug)
|
|
1341
|
-
return page.url.pathname === cleanBase || page.url.pathname === `${cleanBase}/`;
|
|
1342
|
-
// Match exact or subpath otherwise
|
|
1343
|
-
const target = `${cleanBase}/${slug}`;
|
|
1344
|
-
return page.url.pathname === target || page.url.pathname.startsWith(`${target}/`);
|
|
1345
|
-
},
|
|
1346
|
-
/**
|
|
1347
|
-
* Paginates an array.
|
|
1348
|
-
* @template T - The type of array items; defaults to `any`
|
|
1349
|
-
* @param {T[]} array - The array to paginate
|
|
1350
|
-
* @param {number} page - The page number
|
|
1351
|
-
* @param {number} pageSize - The number of items per page; defaults to 10
|
|
1352
|
-
* @returns {Object} The paginated array and total number of pages
|
|
1353
|
-
*/
|
|
1354
|
-
paginateArray: (array, page, pageSize = 10) => {
|
|
1355
|
-
// Define variables
|
|
1356
|
-
const startIndex = (page - 1) * pageSize;
|
|
1357
|
-
const endIndex = startIndex + pageSize;
|
|
1358
|
-
const totalPages = Math.ceil(array.length / pageSize);
|
|
1359
|
-
// Paginate items
|
|
1360
|
-
const paginatedItems = array.slice(startIndex, endIndex);
|
|
1361
|
-
// Return data
|
|
1362
|
-
return { paginatedItems, totalPages };
|
|
1363
|
-
},
|
|
1364
|
-
/**
|
|
1365
|
-
* Parses a `CustomError` object within a page/layout server load function and returns an error to be thrown.
|
|
1366
|
-
* @param {CustomError} error - The error object
|
|
1367
|
-
* @returns A new `Error` object if the error is unexpected, or a Svelte error otherwise
|
|
1368
|
-
*/
|
|
1369
|
-
parseLoadError: (error) => error.code === 500 ? new Error(error.message) : svelteError(error.code, { message: error.message }),
|
|
1370
|
-
/**
|
|
1371
|
-
* Parses a Postgres interval string and returns a string in the specified format.
|
|
1372
|
-
* @param {string} interval - The interval string to parse
|
|
1373
|
-
* @param {"postgres" | "iso" | "iso-short"} type - The format to return the interval in; defaults to `postgres`
|
|
1374
|
-
* @returns A string in the specified format
|
|
1375
|
-
*/
|
|
1376
|
-
parsePostgresInterval: (interval, type = "postgres") => {
|
|
1377
|
-
// Return null if interval is falsy
|
|
1378
|
-
if (!interval)
|
|
1379
|
-
return null;
|
|
1380
|
-
// Parse interval and return in the specified format
|
|
1381
|
-
const { toPostgres, toISOString, toISOStringShort } = parsePgInterval(interval);
|
|
1382
|
-
if (type === "postgres")
|
|
1383
|
-
return toPostgres();
|
|
1384
|
-
if (type === "iso")
|
|
1385
|
-
return toISOString();
|
|
1386
|
-
if (type === "iso-short")
|
|
1387
|
-
return toISOStringShort();
|
|
1388
|
-
// Return null if type is invalid
|
|
1389
|
-
return null;
|
|
1390
|
-
},
|
|
1391
|
-
/**
|
|
1392
|
-
* Parses a `PostgrestError` object and returns a custom error object if any has occurred.
|
|
1393
|
-
* @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
|
|
1394
|
-
* @param {boolean} clientSafe - Whether to clamp 5xx errors down to 422 to prevent Sentry from capturing them as unhandled; defaults to `true`
|
|
1395
|
-
* @returns An object with an `error` if any has occurred
|
|
1396
|
-
*/
|
|
1397
|
-
parsePostgrestError: (postgrestError, clientSafe = true) => {
|
|
1398
|
-
// Return undefined if no error occurred
|
|
1399
|
-
let error;
|
|
1400
|
-
if (!postgrestError)
|
|
1401
|
-
return { error };
|
|
1402
|
-
// Get custom error code from hint
|
|
1403
|
-
const customErrorCode = Number(postgrestError.hint);
|
|
1404
|
-
// Clamp 5xx errors down to 422 to prevent Sentry from capturing them as unhandled; see https://koloseum-technologies.sentry.io/issues/6766267685/
|
|
1405
|
-
let statusCode = clientSafe ? (customErrorCode >= 500 ? 422 : customErrorCode) : customErrorCode;
|
|
1406
|
-
// Return custom error if hint is a number between 400 and 599
|
|
1407
|
-
if (!isNaN(customErrorCode) && customErrorCode >= 400 && customErrorCode <= 599) {
|
|
1408
|
-
error = Utility.customError(statusCode, postgrestError.message);
|
|
1409
|
-
return { error };
|
|
1410
|
-
}
|
|
1411
|
-
// Map Postgrest error codes to custom error codes
|
|
1412
|
-
const errorMap = [
|
|
1413
|
-
{ code: "08", status: 503 },
|
|
1414
|
-
{ code: "09", status: 500 },
|
|
1415
|
-
{ code: "0L", status: 403 },
|
|
1416
|
-
{ code: "0P", status: 403 },
|
|
1417
|
-
{ code: "23503", status: 409 },
|
|
1418
|
-
{ code: "23505", status: 409 },
|
|
1419
|
-
{ code: "25006", status: 405 },
|
|
1420
|
-
{ code: "25", status: 500 },
|
|
1421
|
-
{ code: "28", status: 403 },
|
|
1422
|
-
{ code: "2D", status: 500 },
|
|
1423
|
-
{ code: "38", status: 500 },
|
|
1424
|
-
{ code: "39", status: 500 },
|
|
1425
|
-
{ code: "3B", status: 500 },
|
|
1426
|
-
{ code: "40", status: 500 },
|
|
1427
|
-
{ code: "53400", status: 500 },
|
|
1428
|
-
{ code: "53", status: 503 },
|
|
1429
|
-
{ code: "54", status: 500 },
|
|
1430
|
-
{ code: "55", status: 500 },
|
|
1431
|
-
{ code: "57", status: 500 },
|
|
1432
|
-
{ code: "58", status: 500 },
|
|
1433
|
-
{ code: "F0", status: 500 },
|
|
1434
|
-
{ code: "HV", status: 500 },
|
|
1435
|
-
{ code: "P0001", status: 400 },
|
|
1436
|
-
{ code: "P0", status: 500 },
|
|
1437
|
-
{ code: "XX", status: 500 },
|
|
1438
|
-
{ code: "42883", status: 404 },
|
|
1439
|
-
{ code: "42P01", status: 404 },
|
|
1440
|
-
{ code: "42P17", status: 500 },
|
|
1441
|
-
{ code: "42501", status: 403 }
|
|
1442
|
-
];
|
|
1443
|
-
// Return custom error if Postgrest error code is found
|
|
1444
|
-
for (const { code, status } of errorMap)
|
|
1445
|
-
if (postgrestError.code === code || postgrestError.code.startsWith(code)) {
|
|
1446
|
-
statusCode = clientSafe ? (status >= 500 ? 422 : status) : status;
|
|
1447
|
-
error = Utility.customError(statusCode, Status.ERROR);
|
|
1448
|
-
return { error };
|
|
1449
|
-
}
|
|
1450
|
-
// Return generic error
|
|
1451
|
-
error = Utility.customError(clientSafe ? 422 : 500, Status.ERROR);
|
|
1452
|
-
return { error };
|
|
1453
|
-
},
|
|
1454
|
-
/**
|
|
1455
|
-
* A regular expression for user passwords to match during authentication. It covers the following rules:
|
|
1456
|
-
* - at least 8 characters long
|
|
1457
|
-
* - at least one digit
|
|
1458
|
-
* - at least one lowercase letter
|
|
1459
|
-
* - at least one uppercase letter
|
|
1460
|
-
* - at least one symbol
|
|
1461
|
-
*/
|
|
1462
|
-
passwordRegex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()-_+=|{}[\]:;'<>,.?/~]).{8,}$/,
|
|
1463
|
-
/**
|
|
1464
|
-
* Returns a regular expression for a Koloseum platform ID.
|
|
1465
|
-
* @param {string} type - The type of platform ID to return
|
|
1466
|
-
* @returns A regular expression for the platform ID, or `null` if the type is invalid
|
|
1467
|
-
*/
|
|
1468
|
-
platformIdRegex: (type) => {
|
|
1469
|
-
if (type === "lounge")
|
|
1470
|
-
return /^KL\d{7}$/;
|
|
1471
|
-
if (type === "loungeBranch")
|
|
1472
|
-
return /^KLB\d{7}$/;
|
|
1473
|
-
if (type === "player")
|
|
1474
|
-
return /^KP\d{7}$/;
|
|
1475
|
-
return null;
|
|
1476
|
-
},
|
|
1477
|
-
/**
|
|
1478
|
-
* Sanitises any potential HTML injected into user input.
|
|
1479
|
-
* @param {string} input - The input to be sanitised
|
|
1480
|
-
* @returns A sanitised string, or an empty string if the input is invalid
|
|
1481
|
-
*/
|
|
1482
|
-
sanitiseHtml: (input) => typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} }),
|
|
1483
|
-
/**
|
|
1484
|
-
* Sends a welcome notification to a new user.
|
|
1485
|
-
* @param {SupabaseClient<Database>} supabase - The Supabase client
|
|
1486
|
-
* @param {UserWithCustomMetadata} user - The user to send the notification to
|
|
1487
|
-
* @param {MicroserviceGroup} microserviceGroup - The microservice group the user belongs to
|
|
1488
|
-
* @returns An object with an `error` if any has occurred
|
|
1489
|
-
*/
|
|
1490
|
-
sendWelcomeNotification: async (supabase, user, microserviceGroup) => {
|
|
1491
|
-
// Backroom
|
|
1492
|
-
if (microserviceGroup === "backroom" && !user.user_metadata.backroom?.welcome_notification_sent) {
|
|
1493
|
-
// Use atomic update with timestamp to prevent race conditions
|
|
1494
|
-
const currentTime = new Date().toISOString();
|
|
1495
|
-
const { data: updatedUser, error: updateError } = await supabase.auth.updateUser({
|
|
1496
|
-
data: {
|
|
1497
|
-
backroom: {
|
|
1498
|
-
...user.user_metadata.backroom,
|
|
1499
|
-
welcome_notification_sent: true,
|
|
1500
|
-
welcome_notification_timestamp: currentTime
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
});
|
|
1504
|
-
if (updateError)
|
|
1505
|
-
return { error: Utility.customError(updateError.status ?? 500, updateError.message) };
|
|
1506
|
-
// Only send notification if flag was successfully updated
|
|
1507
|
-
const updatedBackroom = updatedUser.user?.user_metadata?.backroom;
|
|
1508
|
-
if (updatedBackroom?.welcome_notification_sent === true &&
|
|
1509
|
-
updatedBackroom?.welcome_notification_timestamp === currentTime) {
|
|
1510
|
-
// Define SuprSend workflow body
|
|
1511
|
-
const workflowBody = {
|
|
1512
|
-
name: "welcome-to-backroom",
|
|
1513
|
-
template: "welcome-to-backroom",
|
|
1514
|
-
notification_category: "system",
|
|
1515
|
-
users: [
|
|
1516
|
-
{
|
|
1517
|
-
distinct_id: user.id,
|
|
1518
|
-
$skip_create: true
|
|
1519
|
-
}
|
|
1520
|
-
]
|
|
1521
|
-
};
|
|
1522
|
-
// Send welcome notification
|
|
1523
|
-
const { code, error: workflowError } = await Utility.callEdgeFunction(false, supabase, `suprsend/trigger-workflow?user-id=${user.id}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: workflowBody });
|
|
1524
|
-
// If notification fails, revert the flag and return error
|
|
1525
|
-
if (workflowError) {
|
|
1526
|
-
await supabase.auth.updateUser({
|
|
1527
|
-
data: {
|
|
1528
|
-
backroom: {
|
|
1529
|
-
...user.user_metadata.backroom,
|
|
1530
|
-
welcome_notification_sent: false,
|
|
1531
|
-
welcome_notification_timestamp: undefined
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1535
|
-
return { error: Utility.customError(code, workflowError) };
|
|
1536
|
-
}
|
|
1537
|
-
// Update user object in memory
|
|
1538
|
-
user.user_metadata.backroom = updatedBackroom;
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
// Return empty object
|
|
1542
|
-
return {};
|
|
1543
|
-
},
|
|
1544
|
-
/**
|
|
1545
|
-
* A regular expression for social media handles, without a leading slash or @ character.
|
|
1546
|
-
*
|
|
1547
|
-
* - Source: https://stackoverflow.com/a/74579651
|
|
1548
|
-
*/
|
|
1549
|
-
socialMediaHandleRegex: /^([A-Za-z0-9_.]{3,25})$/gm,
|
|
1550
|
-
/**
|
|
1551
|
-
* Validates address data submitted in a form and returns the validated data.
|
|
1552
|
-
* @param {FormData} formData - The submitted form data
|
|
1553
|
-
* @returns An object with the validated `address`, or an `error` if any has occurred
|
|
1554
|
-
*/
|
|
1555
|
-
validateAddress: async (formData) => {
|
|
1556
|
-
// Create Kenya administrative divisions instance
|
|
1557
|
-
const kenyaAdmin = new KenyaAdministrativeDivisions();
|
|
1558
|
-
// Building name
|
|
1559
|
-
let buildingName = formData.get("building-name");
|
|
1560
|
-
if (!buildingName)
|
|
1561
|
-
return { error: Utility.customError(400, "Building name is required.") };
|
|
1562
|
-
buildingName = Utility.sanitiseHtml(trim(escape(buildingName)));
|
|
1563
|
-
// Unit number
|
|
1564
|
-
let unitNumber = formData.get("unit-number") || undefined;
|
|
1565
|
-
if (unitNumber) {
|
|
1566
|
-
unitNumber = Utility.sanitiseHtml(trim(escape(unitNumber)));
|
|
1567
|
-
if (unitNumber === "")
|
|
1568
|
-
unitNumber = undefined;
|
|
1569
|
-
}
|
|
1570
|
-
// Street name
|
|
1571
|
-
let streetName = formData.get("street-name");
|
|
1572
|
-
if (!streetName)
|
|
1573
|
-
return { error: Utility.customError(400, "Street name is required.") };
|
|
1574
|
-
streetName = Utility.sanitiseHtml(trim(escape(streetName)));
|
|
1575
|
-
// P.O. box and postal code
|
|
1576
|
-
let boxNumber = formData.get("box-number") || undefined;
|
|
1577
|
-
let postalCode = formData.get("postal-code") || undefined;
|
|
1578
|
-
if (boxNumber) {
|
|
1579
|
-
boxNumber = Utility.sanitiseHtml(trim(escape(boxNumber)));
|
|
1580
|
-
if (boxNumber === "")
|
|
1581
|
-
boxNumber = undefined;
|
|
1582
|
-
if (boxNumber && !postalCode)
|
|
1583
|
-
return { error: Utility.customError(400, "Postal code is required.") };
|
|
1584
|
-
}
|
|
1585
|
-
if (postalCode) {
|
|
1586
|
-
postalCode = Utility.sanitiseHtml(trim(escape(postalCode)));
|
|
1587
|
-
if (postalCode === "")
|
|
1588
|
-
postalCode = undefined;
|
|
1589
|
-
if (postalCode && !boxNumber)
|
|
1590
|
-
return { error: Utility.customError(400, "P.O. box is required.") };
|
|
1591
|
-
}
|
|
1592
|
-
// Town
|
|
1593
|
-
let town = formData.get("town");
|
|
1594
|
-
if (!town)
|
|
1595
|
-
return { error: Utility.customError(400, "City/town is required.") };
|
|
1596
|
-
town = Utility.sanitiseHtml(trim(escape(town)));
|
|
1597
|
-
// County
|
|
1598
|
-
let county = undefined;
|
|
1599
|
-
const countyCode = Number(formData.get("county-code"));
|
|
1600
|
-
if (isNaN(countyCode))
|
|
1601
|
-
return { error: Utility.customError(400, "County is invalid.") };
|
|
1602
|
-
if (!countyCode)
|
|
1603
|
-
return { error: Utility.customError(400, "County is required.") };
|
|
1604
|
-
const counties = await kenyaAdmin.getAll();
|
|
1605
|
-
if (!counties || counties.length === 0)
|
|
1606
|
-
return { error: Utility.customError(400, "County is invalid.") };
|
|
1607
|
-
const countyData = counties.find(({ county_code }) => county_code === countyCode);
|
|
1608
|
-
if (!countyData)
|
|
1609
|
-
return { error: Utility.customError(400, "County is invalid.") };
|
|
1610
|
-
const { county_name, constituencies } = countyData;
|
|
1611
|
-
county = county_name;
|
|
1612
|
-
// Sub-county
|
|
1613
|
-
let subCounty = formData.get("sub-county") || undefined;
|
|
1614
|
-
if (subCounty) {
|
|
1615
|
-
subCounty = Utility.sanitiseHtml(trim(escape(subCounty)));
|
|
1616
|
-
if (subCounty === "")
|
|
1617
|
-
subCounty = undefined;
|
|
1618
|
-
else {
|
|
1619
|
-
// Check if constituencies exists and is an array before mapping
|
|
1620
|
-
if (!constituencies || !Array.isArray(constituencies))
|
|
1621
|
-
return { error: Utility.customError(400, "Sub-county data is unavailable.") };
|
|
1622
|
-
// Get list of sub-counties
|
|
1623
|
-
const subCounties = constituencies.map(({ constituency_name: name }) => name);
|
|
1624
|
-
if (!subCounties.includes(subCounty))
|
|
1625
|
-
return { error: Utility.customError(400, "Sub-county is invalid.") };
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
// Additional information
|
|
1629
|
-
let additionalInfo = formData.get("address-additional-info") || undefined;
|
|
1630
|
-
if (additionalInfo) {
|
|
1631
|
-
additionalInfo = Utility.sanitiseHtml(trim(escape(additionalInfo)));
|
|
1632
|
-
if (additionalInfo === "")
|
|
1633
|
-
additionalInfo = undefined;
|
|
1634
|
-
}
|
|
1635
|
-
// Create address object
|
|
1636
|
-
const address = {
|
|
1637
|
-
buildingName,
|
|
1638
|
-
unitNumber,
|
|
1639
|
-
streetName,
|
|
1640
|
-
town,
|
|
1641
|
-
subCounty,
|
|
1642
|
-
county,
|
|
1643
|
-
additionalInfo
|
|
1644
|
-
};
|
|
1645
|
-
// Return data
|
|
1646
|
-
return { address };
|
|
1647
|
-
},
|
|
1648
|
-
/**
|
|
1649
|
-
* Validates an E.164 phone number.
|
|
1650
|
-
* @param {string} phoneNumber - The phone number to be validated
|
|
1651
|
-
* @param {"any" | MobilePhoneLocale | MobilePhoneLocale[]} [locale="any"] - The locale(s) to validate the phone number against
|
|
1652
|
-
* @returns `true` if the phone number is valid; `false` otherwise
|
|
1653
|
-
*/
|
|
1654
|
-
validateE164: (phoneNumber, locale = "en-KE") => isMobilePhone(phoneNumber, locale) && Boolean(phoneNumber.match(Utility.e164Regex)),
|
|
1655
|
-
/**
|
|
1656
|
-
* Validates a social media handle.
|
|
1657
|
-
* @param {string} handle - The social media handle to be validated
|
|
1658
|
-
* @returns `true` if the handle is valid; `false` otherwise
|
|
1659
|
-
*/
|
|
1660
|
-
validateSocialMediaHandle: (handle) => !isURL(handle, { require_protocol: false }) &&
|
|
1661
|
-
!handle.startsWith("@") &&
|
|
1662
|
-
Boolean(handle.match(Utility.socialMediaHandleRegex))
|
|
1663
|
-
};
|
|
1664
|
-
/* CACHE FUNCTIONS */
|
|
1665
|
-
export const Cache = {
|
|
1666
|
-
/**
|
|
1667
|
-
* Delete cached data from Valkey.
|
|
1668
|
-
* @param valkey - The Valkey client instance
|
|
1669
|
-
* @param cachePrefix - The cache prefix
|
|
1670
|
-
* @param key - The cache key
|
|
1671
|
-
*/
|
|
1672
|
-
deleteData: async (valkey, cachePrefix, key) => {
|
|
1673
|
-
try {
|
|
1674
|
-
await valkey.del(`${cachePrefix}${key}`);
|
|
1675
|
-
}
|
|
1676
|
-
catch (error) {
|
|
1677
|
-
console.error("Cache delete error:", error);
|
|
1678
|
-
}
|
|
1679
|
-
},
|
|
1680
|
-
/**
|
|
1681
|
-
* Generate cache key with consistent formatting
|
|
1682
|
-
* @param prefix - The key prefix
|
|
1683
|
-
* @param params - Additional parameters to include in the key
|
|
1684
|
-
* @returns The formatted cache key
|
|
1685
|
-
*/
|
|
1686
|
-
generateDataKey: (prefix, ...params) => `${prefix}:${params.join(":")}`,
|
|
1687
|
-
/**
|
|
1688
|
-
* Get cached data from Valkey.
|
|
1689
|
-
* @param valkey - The Valkey client instance
|
|
1690
|
-
* @param cachePrefix - The cache prefix
|
|
1691
|
-
* @param key - The cache key
|
|
1692
|
-
* @returns The cached data, or `null` if not found
|
|
1693
|
-
*/
|
|
1694
|
-
getData: async (valkey, cachePrefix, key) => {
|
|
1695
|
-
try {
|
|
1696
|
-
const cached = await valkey.get(`${cachePrefix}${key}`);
|
|
1697
|
-
return cached ? JSON.parse(cached) : null;
|
|
1698
|
-
}
|
|
1699
|
-
catch (error) {
|
|
1700
|
-
console.error("Cache get error:", error);
|
|
1701
|
-
return null;
|
|
1702
|
-
}
|
|
1703
|
-
},
|
|
1704
|
-
/**
|
|
1705
|
-
* Invalidate cache by pattern
|
|
1706
|
-
* @param valkey - The Valkey client instance
|
|
1707
|
-
* @param cachePrefix - The cache prefix
|
|
1708
|
-
* @param pattern - The pattern to match keys against (e.g., "app-config*")
|
|
1709
|
-
*/
|
|
1710
|
-
invalidateDataByPattern: async (valkey, cachePrefix, pattern) => {
|
|
1711
|
-
try {
|
|
1712
|
-
const keys = await valkey.keys(`${cachePrefix}${pattern}`);
|
|
1713
|
-
if (keys.length > 0)
|
|
1714
|
-
await valkey.del(...keys);
|
|
1715
|
-
}
|
|
1716
|
-
catch (error) {
|
|
1717
|
-
console.error("Cache pattern invalidation error:", error);
|
|
1718
|
-
}
|
|
1719
|
-
},
|
|
1720
|
-
/**
|
|
1721
|
-
* Set data in Valkey cache.
|
|
1722
|
-
* @param valkey - The Valkey client instance
|
|
1723
|
-
* @param cachePrefix - The cache prefix
|
|
1724
|
-
* @param key - The cache key
|
|
1725
|
-
* @param data - The data to cache
|
|
1726
|
-
* @param ttl - The time to live in seconds; defaults to 1 hour
|
|
1727
|
-
*/
|
|
1728
|
-
setData: async (valkey, cachePrefix, key, data, ttl = 3600) => {
|
|
1729
|
-
try {
|
|
1730
|
-
await valkey.setex(`${cachePrefix}${key}`, ttl, JSON.stringify(data));
|
|
1731
|
-
}
|
|
1732
|
-
catch (error) {
|
|
1733
|
-
console.error("Cache set error:", error);
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
};
|
|
1737
|
-
/* BROWSER COMPATIBILITY FUNCTIONS */
|
|
1738
|
-
export const Browser = {
|
|
1739
|
-
/**
|
|
1740
|
-
* Checks if a specific feature is supported by the browser.
|
|
1741
|
-
* @param feature - The feature to check
|
|
1742
|
-
* @param browserName - The name of the browser
|
|
1743
|
-
* @param browserVersion - The version of the browser
|
|
1744
|
-
* @returns `true` if the feature is supported, `false` otherwise
|
|
1745
|
-
*/
|
|
1746
|
-
checkFeatureSupport: (feature, browserName, browserVersion) => {
|
|
1747
|
-
// Feature support matrix based on browser versions
|
|
1748
|
-
const featureSupport = {
|
|
1749
|
-
optionalChaining: {
|
|
1750
|
-
Chrome: 80,
|
|
1751
|
-
Firefox: 74,
|
|
1752
|
-
Safari: 13.4,
|
|
1753
|
-
Edge: 80,
|
|
1754
|
-
Opera: 80
|
|
1755
|
-
},
|
|
1756
|
-
nullishCoalescing: {
|
|
1757
|
-
Chrome: 80,
|
|
1758
|
-
Firefox: 72,
|
|
1759
|
-
Safari: 13.4,
|
|
1760
|
-
Edge: 80,
|
|
1761
|
-
Opera: 80
|
|
1762
|
-
},
|
|
1763
|
-
fetch: {
|
|
1764
|
-
Chrome: 42,
|
|
1765
|
-
Firefox: 39,
|
|
1766
|
-
Safari: 10.1,
|
|
1767
|
-
Edge: 14,
|
|
1768
|
-
Opera: 29
|
|
1769
|
-
},
|
|
1770
|
-
es6Modules: {
|
|
1771
|
-
Chrome: 61,
|
|
1772
|
-
Firefox: 60,
|
|
1773
|
-
Safari: 10.1,
|
|
1774
|
-
Edge: 16,
|
|
1775
|
-
Opera: 48
|
|
1776
|
-
},
|
|
1777
|
-
asyncAwait: {
|
|
1778
|
-
Chrome: 55,
|
|
1779
|
-
Firefox: 52,
|
|
1780
|
-
Safari: 10.1,
|
|
1781
|
-
Edge: 14,
|
|
1782
|
-
Opera: 42
|
|
1783
|
-
}
|
|
1784
|
-
};
|
|
1785
|
-
const featureMatrix = featureSupport[feature];
|
|
1786
|
-
if (!featureMatrix)
|
|
1787
|
-
return true; // Unknown feature, assume supported
|
|
1788
|
-
const minVersion = featureMatrix[browserName];
|
|
1789
|
-
if (!minVersion)
|
|
1790
|
-
return true; // Unknown browser, assume supported
|
|
1791
|
-
return browserVersion >= minVersion;
|
|
1792
|
-
},
|
|
1793
|
-
/**
|
|
1794
|
-
* Gets detailed browser information for debugging.
|
|
1795
|
-
*/
|
|
1796
|
-
getDetailedInfo: () => {
|
|
1797
|
-
if (typeof window === "undefined") {
|
|
1798
|
-
return {
|
|
1799
|
-
browser: "Server-side rendering",
|
|
1800
|
-
os: "Unknown",
|
|
1801
|
-
platform: "Unknown",
|
|
1802
|
-
engine: "Unknown",
|
|
1803
|
-
userAgent: "Unknown"
|
|
1804
|
-
};
|
|
1805
|
-
}
|
|
1806
|
-
const browser = Bowser.getParser(window.navigator.userAgent);
|
|
1807
|
-
return {
|
|
1808
|
-
browser: `${browser.getBrowserName()} ${browser.getBrowserVersion()}`,
|
|
1809
|
-
os: `${browser.getOSName()} ${browser.getOSVersion()}`,
|
|
1810
|
-
platform: browser.getPlatformType(),
|
|
1811
|
-
engine: browser.getEngineName(),
|
|
1812
|
-
userAgent: window.navigator.userAgent
|
|
1813
|
-
};
|
|
1814
|
-
},
|
|
1815
|
-
/**
|
|
1816
|
-
* Gets comprehensive browser information using Bowser.
|
|
1817
|
-
*/
|
|
1818
|
-
getInfo: () => {
|
|
1819
|
-
if (typeof window === "undefined") {
|
|
1820
|
-
return {
|
|
1821
|
-
name: "server",
|
|
1822
|
-
version: 0,
|
|
1823
|
-
isOldSafari: false,
|
|
1824
|
-
isIOS: false,
|
|
1825
|
-
supportsOptionalChaining: true,
|
|
1826
|
-
supportsNullishCoalescing: true,
|
|
1827
|
-
supportsFetch: true
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
const browser = Bowser.getParser(window.navigator.userAgent);
|
|
1831
|
-
const browserName = browser.getBrowserName();
|
|
1832
|
-
const browserVersion = parseFloat(browser.getBrowserVersion() || "0");
|
|
1833
|
-
const osName = browser.getOSName();
|
|
1834
|
-
const isIOS = osName === "iOS";
|
|
1835
|
-
const isSafari = browserName === "Safari";
|
|
1836
|
-
const isOldSafari = isSafari && browserVersion < 13.4;
|
|
1837
|
-
return {
|
|
1838
|
-
name: browserName,
|
|
1839
|
-
version: browserVersion,
|
|
1840
|
-
isOldSafari,
|
|
1841
|
-
isIOS,
|
|
1842
|
-
supportsOptionalChaining: Browser.checkFeatureSupport("optionalChaining", browserName, browserVersion),
|
|
1843
|
-
supportsNullishCoalescing: Browser.checkFeatureSupport("nullishCoalescing", browserName, browserVersion),
|
|
1844
|
-
supportsFetch: Browser.checkFeatureSupport("fetch", browserName, browserVersion)
|
|
1845
|
-
};
|
|
1846
|
-
},
|
|
1847
|
-
/**
|
|
1848
|
-
* Gets specific browser recommendations based on the detected browser.
|
|
1849
|
-
*/
|
|
1850
|
-
getRecommendations: () => {
|
|
1851
|
-
if (typeof window === "undefined")
|
|
1852
|
-
return {
|
|
1853
|
-
title: "Browser compatibility issue",
|
|
1854
|
-
message: "Browser detection is not available on theserver.",
|
|
1855
|
-
recommendations: [],
|
|
1856
|
-
critical: false
|
|
1857
|
-
};
|
|
1858
|
-
const browser = Bowser.getParser(window.navigator.userAgent);
|
|
1859
|
-
const browserName = browser.getBrowserName();
|
|
1860
|
-
const browserVersion = browser.getBrowserVersion();
|
|
1861
|
-
// Internet Explorer
|
|
1862
|
-
if (browserName === "Internet Explorer") {
|
|
1863
|
-
return {
|
|
1864
|
-
title: "Unsupported browser",
|
|
1865
|
-
message: "Internet Explorer is not supported. Please use a modern browser.",
|
|
1866
|
-
recommendations: [
|
|
1867
|
-
"Download and install Microsoft Edge",
|
|
1868
|
-
"Use an alternative such as Chrome or Firefox (or Safari on Mac)"
|
|
1869
|
-
],
|
|
1870
|
-
critical: true
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
// Apple Safari
|
|
1874
|
-
if (browserName === "Safari" && parseFloat(browserVersion || "0") < 13.4) {
|
|
1875
|
-
return {
|
|
1876
|
-
title: "Browser compatibility issue",
|
|
1877
|
-
message: `You're using Safari ${browserVersion || "Unknown"}, which doesn't support modern web features.`,
|
|
1878
|
-
recommendations: [
|
|
1879
|
-
"Update to Safari 13.4 or later",
|
|
1880
|
-
"Use Chrome or Firefox as an alternative",
|
|
1881
|
-
"Update your iOS device to the latest version"
|
|
1882
|
-
],
|
|
1883
|
-
critical: true
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
// Google Chrome
|
|
1887
|
-
if (browserName === "Chrome" && parseFloat(browserVersion || "0") < 80) {
|
|
1888
|
-
return {
|
|
1889
|
-
title: "Outdated browser",
|
|
1890
|
-
message: `You're using Chrome ${browserVersion || "Unknown"}, which may not support all features.`,
|
|
1891
|
-
recommendations: ["Update Chrome to version 80 or later", "Enable automatic updates in Chrome settings"],
|
|
1892
|
-
critical: false
|
|
1893
|
-
};
|
|
1894
|
-
}
|
|
1895
|
-
// Mozilla Firefox
|
|
1896
|
-
if (browserName === "Firefox" && parseFloat(browserVersion || "0") < 80) {
|
|
1897
|
-
return {
|
|
1898
|
-
title: "Outdated browser",
|
|
1899
|
-
message: `You're using Firefox ${browserVersion || "Unknown"}, which may not support all features.`,
|
|
1900
|
-
recommendations: ["Update Firefox to version 80 or later", "Enable automatic updates in Firefox settings"],
|
|
1901
|
-
critical: false
|
|
1902
|
-
};
|
|
1903
|
-
}
|
|
1904
|
-
// Microsoft Edge
|
|
1905
|
-
if (browserName === "Edge" && parseFloat(browserVersion || "0") < 80) {
|
|
1906
|
-
return {
|
|
1907
|
-
title: "Outdated browser",
|
|
1908
|
-
message: `You're using Edge ${browserVersion || "Unknown"}, which may not support all features.`,
|
|
1909
|
-
recommendations: ["Update Edge to version 80 or later", "Enable automatic updates in Edge settings"],
|
|
1910
|
-
critical: false
|
|
1911
|
-
};
|
|
1912
|
-
}
|
|
1913
|
-
// For unknown or very old browsers
|
|
1914
|
-
if (browserName === "Unknown" || parseFloat(browserVersion || "0") < 50) {
|
|
1915
|
-
return {
|
|
1916
|
-
title: "Unrecognised browser",
|
|
1917
|
-
message: "Your browser may not be fully supported. For the best experience, please use a modern browser.",
|
|
1918
|
-
recommendations: ["Use Chrome, Firefox or Edge (latest version)", "Use Safari 13.4+ (on Mac/iOS)"],
|
|
1919
|
-
critical: true
|
|
1920
|
-
};
|
|
1921
|
-
}
|
|
1922
|
-
// Default for modern browsers
|
|
1923
|
-
return {
|
|
1924
|
-
title: "Browser compatibility issue",
|
|
1925
|
-
message: "Your browser may not fully support this application.",
|
|
1926
|
-
recommendations: [
|
|
1927
|
-
"Update your browser to the latest version",
|
|
1928
|
-
"Clear your browser cache and cookies",
|
|
1929
|
-
"Disable browser extensions temporarily",
|
|
1930
|
-
"Try a different modern browser"
|
|
1931
|
-
],
|
|
1932
|
-
critical: false
|
|
1933
|
-
};
|
|
1934
|
-
},
|
|
1935
|
-
/**
|
|
1936
|
-
* Checks if the current browser is compatible with the application.
|
|
1937
|
-
*/
|
|
1938
|
-
isBrowserCompatible: () => {
|
|
1939
|
-
if (typeof window === "undefined")
|
|
1940
|
-
return true;
|
|
1941
|
-
const browser = Bowser.getParser(window.navigator.userAgent);
|
|
1942
|
-
const browserName = browser.getBrowserName();
|
|
1943
|
-
const browserVersion = parseFloat(browser.getBrowserVersion() || "0");
|
|
1944
|
-
// Define minimum supported versions
|
|
1945
|
-
const minVersions = {
|
|
1946
|
-
"Internet Explorer": 0, // Not supported at all
|
|
1947
|
-
Safari: 13.4,
|
|
1948
|
-
Chrome: 80,
|
|
1949
|
-
Firefox: 80,
|
|
1950
|
-
Edge: 80,
|
|
1951
|
-
Opera: 80
|
|
1952
|
-
};
|
|
1953
|
-
// Internet Explorer is never compatible
|
|
1954
|
-
if (browserName === "Internet Explorer") {
|
|
1955
|
-
return false;
|
|
1956
|
-
}
|
|
1957
|
-
// Check if browser version meets minimum requirements
|
|
1958
|
-
const minVersion = minVersions[browserName];
|
|
1959
|
-
if (minVersion && browserVersion < minVersion) {
|
|
1960
|
-
return false;
|
|
1961
|
-
}
|
|
1962
|
-
// Check for critical modern features
|
|
1963
|
-
return (Browser.checkFeatureSupport("optionalChaining", browserName, browserVersion) &&
|
|
1964
|
-
Browser.checkFeatureSupport("nullishCoalescing", browserName, browserVersion) &&
|
|
1965
|
-
Browser.checkFeatureSupport("fetch", browserName, browserVersion) &&
|
|
1966
|
-
Browser.checkFeatureSupport("es6Modules", browserName, browserVersion) &&
|
|
1967
|
-
Browser.checkFeatureSupport("asyncAwait", browserName, browserVersion));
|
|
1968
|
-
},
|
|
1969
|
-
/**
|
|
1970
|
-
* Checks if the browser is Internet Explorer.
|
|
1971
|
-
*/
|
|
1972
|
-
isInternetExplorer: () => {
|
|
1973
|
-
if (typeof window === "undefined")
|
|
1974
|
-
return false;
|
|
1975
|
-
const browser = Bowser.getParser(window.navigator.userAgent);
|
|
1976
|
-
return browser.getBrowserName() === "Internet Explorer";
|
|
1977
|
-
},
|
|
1978
|
-
/**
|
|
1979
|
-
* Checks if the browser is iOS Safari.
|
|
1980
|
-
*/
|
|
1981
|
-
isIOSSafari: () => {
|
|
1982
|
-
if (typeof window === "undefined")
|
|
1983
|
-
return false;
|
|
1984
|
-
const browser = Bowser.getParser(window.navigator.userAgent);
|
|
1985
|
-
return browser.getOSName() === "iOS" && browser.getBrowserName() === "Safari";
|
|
1986
|
-
},
|
|
1987
|
-
/**
|
|
1988
|
-
* Checks if the browser is a legacy version that needs updating.
|
|
1989
|
-
*/
|
|
1990
|
-
isLegacyBrowser: () => {
|
|
1991
|
-
if (typeof window === "undefined")
|
|
1992
|
-
return false;
|
|
1993
|
-
const browser = Bowser.getParser(window.navigator.userAgent);
|
|
1994
|
-
const browserName = browser.getBrowserName();
|
|
1995
|
-
const browserVersion = parseFloat(browser.getBrowserVersion() || "0");
|
|
1996
|
-
// Legacy browser thresholds
|
|
1997
|
-
const legacyThresholds = {
|
|
1998
|
-
"Internet Explorer": 0, // All versions are legacy
|
|
1999
|
-
Safari: 13.4,
|
|
2000
|
-
Chrome: 80,
|
|
2001
|
-
Firefox: 80,
|
|
2002
|
-
Edge: 80,
|
|
2003
|
-
Opera: 80
|
|
2004
|
-
};
|
|
2005
|
-
const threshold = legacyThresholds[browserName];
|
|
2006
|
-
return threshold ? browserVersion < threshold : false;
|
|
2007
|
-
},
|
|
2008
|
-
/**
|
|
2009
|
-
* Checks if the browser is an old version of Safari.
|
|
2010
|
-
*/
|
|
2011
|
-
isOldSafari: () => {
|
|
2012
|
-
if (typeof window === "undefined")
|
|
2013
|
-
return false;
|
|
2014
|
-
const browser = Bowser.getParser(window.navigator.userAgent);
|
|
2015
|
-
const browserName = browser.getBrowserName();
|
|
2016
|
-
const browserVersion = parseFloat(browser.getBrowserVersion() || "0");
|
|
2017
|
-
return browserName === "Safari" && browserVersion < 13.4;
|
|
2018
|
-
}
|
|
2019
|
-
};
|