@edgedev/firebase 2.2.81 → 2.2.83
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/package.json +1 -1
- package/src/cms.js +284 -32
- package/src/edgeFirebase.js +124 -124
package/package.json
CHANGED
package/src/cms.js
CHANGED
|
@@ -235,16 +235,76 @@ const normalizeDomain = (value) => {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
normalized = normalized.split('/')[0] || ''
|
|
238
|
+
if (normalized.startsWith('[')) {
|
|
239
|
+
const closingIndex = normalized.indexOf(']')
|
|
240
|
+
if (closingIndex !== -1)
|
|
241
|
+
normalized = normalized.slice(0, closingIndex + 1)
|
|
242
|
+
}
|
|
238
243
|
if (normalized.includes(':') && !normalized.startsWith('[')) {
|
|
239
244
|
normalized = normalized.split(':')[0] || ''
|
|
240
245
|
}
|
|
241
246
|
return normalized.replace(/\.+$/g, '')
|
|
242
247
|
}
|
|
243
248
|
|
|
249
|
+
const stripIpv6Brackets = (value) => {
|
|
250
|
+
const text = String(value || '').trim()
|
|
251
|
+
if (text.startsWith('[') && text.endsWith(']'))
|
|
252
|
+
return text.slice(1, -1)
|
|
253
|
+
return text
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const isIpv4Address = (value) => {
|
|
257
|
+
const parts = String(value || '').split('.')
|
|
258
|
+
if (parts.length !== 4)
|
|
259
|
+
return false
|
|
260
|
+
return parts.every((part) => {
|
|
261
|
+
if (!/^\d{1,3}$/.test(part))
|
|
262
|
+
return false
|
|
263
|
+
const num = Number(part)
|
|
264
|
+
return num >= 0 && num <= 255
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const isIpv6Address = (value) => {
|
|
269
|
+
const normalized = String(value || '').toLowerCase()
|
|
270
|
+
if (!normalized.includes(':'))
|
|
271
|
+
return false
|
|
272
|
+
return /^[0-9a-f:]+$/.test(normalized)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const isIpAddress = (value) => {
|
|
276
|
+
if (!value)
|
|
277
|
+
return false
|
|
278
|
+
const normalized = stripIpv6Brackets(value)
|
|
279
|
+
return isIpv4Address(normalized) || isIpv6Address(normalized)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const getCloudflareApexDomain = (domain) => {
|
|
283
|
+
if (!domain)
|
|
284
|
+
return ''
|
|
285
|
+
if (domain.startsWith('www.'))
|
|
286
|
+
return domain.slice(4)
|
|
287
|
+
return domain
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const shouldDisplayDomainDnsRecords = (domain) => {
|
|
291
|
+
const normalizedDomain = normalizeDomain(domain)
|
|
292
|
+
const apexDomain = getCloudflareApexDomain(normalizedDomain)
|
|
293
|
+
if (!apexDomain)
|
|
294
|
+
return false
|
|
295
|
+
if (apexDomain === 'localhost' || apexDomain.endsWith('.localhost'))
|
|
296
|
+
return false
|
|
297
|
+
if (isIpAddress(apexDomain))
|
|
298
|
+
return false
|
|
299
|
+
if (apexDomain.endsWith('.dev'))
|
|
300
|
+
return false
|
|
301
|
+
return true
|
|
302
|
+
}
|
|
303
|
+
|
|
244
304
|
const shouldSyncCloudflareDomain = (domain) => {
|
|
245
305
|
if (!domain)
|
|
246
306
|
return false
|
|
247
|
-
if (domain
|
|
307
|
+
if (!shouldDisplayDomainDnsRecords(domain))
|
|
248
308
|
return false
|
|
249
309
|
if (CLOUDFLARE_PAGES_PROJECT) {
|
|
250
310
|
const pagesDomain = `${CLOUDFLARE_PAGES_PROJECT}.pages.dev`
|
|
@@ -262,6 +322,44 @@ const getCloudflarePagesDomain = (domain) => {
|
|
|
262
322
|
return `www.${domain}`
|
|
263
323
|
}
|
|
264
324
|
|
|
325
|
+
const getCloudflarePagesTarget = () => {
|
|
326
|
+
if (!CLOUDFLARE_PAGES_PROJECT)
|
|
327
|
+
return ''
|
|
328
|
+
return `${CLOUDFLARE_PAGES_PROJECT}.pages.dev`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const buildDomainDnsPayload = (domain, pagesTarget = '') => {
|
|
332
|
+
const normalizedDomain = normalizeDomain(domain)
|
|
333
|
+
const apexDomain = getCloudflareApexDomain(normalizedDomain)
|
|
334
|
+
const wwwDomain = getCloudflarePagesDomain(apexDomain)
|
|
335
|
+
const target = pagesTarget || getCloudflarePagesTarget()
|
|
336
|
+
const dnsEligible = shouldDisplayDomainDnsRecords(apexDomain)
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
domain: normalizedDomain,
|
|
340
|
+
apexDomain,
|
|
341
|
+
wwwDomain,
|
|
342
|
+
dnsEligible,
|
|
343
|
+
dnsRecords: {
|
|
344
|
+
target,
|
|
345
|
+
www: {
|
|
346
|
+
type: 'CNAME',
|
|
347
|
+
name: 'www',
|
|
348
|
+
host: wwwDomain,
|
|
349
|
+
value: target,
|
|
350
|
+
enabled: dnsEligible && !!target,
|
|
351
|
+
},
|
|
352
|
+
apex: {
|
|
353
|
+
type: 'CNAME',
|
|
354
|
+
name: '@',
|
|
355
|
+
host: apexDomain,
|
|
356
|
+
value: target,
|
|
357
|
+
enabled: dnsEligible && !!target,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
265
363
|
const isCloudflareDomainAlreadyExistsError = (status, errors = [], message = '') => {
|
|
266
364
|
if (status === 409)
|
|
267
365
|
return true
|
|
@@ -1839,12 +1937,18 @@ const buildBlockAiPrompt = ({
|
|
|
1839
1937
|
].join('\n')
|
|
1840
1938
|
}
|
|
1841
1939
|
|
|
1940
|
+
const assertCallableUser = (request) => {
|
|
1941
|
+
if (!request?.auth?.uid)
|
|
1942
|
+
throw new HttpsError('unauthenticated', 'Authentication required.')
|
|
1943
|
+
if (request?.data?.uid !== request.auth.uid)
|
|
1944
|
+
throw new HttpsError('permission-denied', 'UID mismatch.')
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1842
1947
|
exports.updateSeoFromAi = onCall({ timeoutSeconds: 180 }, async (request) => {
|
|
1948
|
+
assertCallableUser(request)
|
|
1843
1949
|
const data = request.data || {}
|
|
1844
1950
|
const auth = request.auth
|
|
1845
|
-
const { orgId, siteId, pageId
|
|
1846
|
-
if (!auth?.uid || auth.uid !== uid)
|
|
1847
|
-
throw new HttpsError('permission-denied', 'Unauthorized')
|
|
1951
|
+
const { orgId, siteId, pageId } = data
|
|
1848
1952
|
if (!orgId || !siteId || !pageId)
|
|
1849
1953
|
throw new HttpsError('invalid-argument', 'Missing orgId, siteId, or pageId')
|
|
1850
1954
|
const allowed = await permissionCheck(auth.uid, 'write', `organizations/${orgId}/sites/${siteId}/pages`)
|
|
@@ -1910,25 +2014,72 @@ exports.updateSeoFromAi = onCall({ timeoutSeconds: 180 }, async (request) => {
|
|
|
1910
2014
|
})
|
|
1911
2015
|
|
|
1912
2016
|
exports.getCloudflarePagesProject = onCall(async (request) => {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2017
|
+
assertCallableUser(request)
|
|
2018
|
+
const data = request.data || {}
|
|
2019
|
+
const orgId = String(data.orgId || '').trim()
|
|
2020
|
+
const siteId = String(data.siteId || '').trim()
|
|
2021
|
+
const rawDomains = Array.isArray(data.domains) ? data.domains : []
|
|
2022
|
+
const normalizedDomains = Array.from(new Set(rawDomains.map(normalizeDomain).filter(Boolean)))
|
|
2023
|
+
const pagesTarget = getCloudflarePagesTarget()
|
|
1916
2024
|
|
|
1917
|
-
if (!CLOUDFLARE_PAGES_PROJECT)
|
|
2025
|
+
if (!CLOUDFLARE_PAGES_PROJECT)
|
|
1918
2026
|
logger.warn('CLOUDFLARE_PAGES_PROJECT is not set.')
|
|
1919
|
-
|
|
2027
|
+
|
|
2028
|
+
const domainRegistry = {}
|
|
2029
|
+
if (orgId && siteId && normalizedDomains.length) {
|
|
2030
|
+
const allowed = await permissionCheck(request.auth.uid, 'read', `organizations/${orgId}/sites`)
|
|
2031
|
+
if (!allowed)
|
|
2032
|
+
throw new HttpsError('permission-denied', 'Not allowed to read site settings')
|
|
2033
|
+
|
|
2034
|
+
await Promise.all(normalizedDomains.map(async (domain) => {
|
|
2035
|
+
const registryRef = db.collection(DOMAIN_REGISTRY_COLLECTION).doc(domain)
|
|
2036
|
+
const registrySnap = await registryRef.get()
|
|
2037
|
+
const fallback = buildDomainDnsPayload(domain, pagesTarget)
|
|
2038
|
+
if (!registrySnap.exists) {
|
|
2039
|
+
domainRegistry[domain] = {
|
|
2040
|
+
...fallback,
|
|
2041
|
+
apexAttempted: false,
|
|
2042
|
+
apexAdded: false,
|
|
2043
|
+
apexError: '',
|
|
2044
|
+
dnsGuidance: fallback.dnsEligible
|
|
2045
|
+
? 'Add the www CNAME. Apex is unavailable; forward apex to www.'
|
|
2046
|
+
: 'DNS records are not shown for localhost, IP addresses, or .dev domains.',
|
|
2047
|
+
}
|
|
2048
|
+
return
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
const value = registrySnap.data() || {}
|
|
2052
|
+
domainRegistry[domain] = {
|
|
2053
|
+
...fallback,
|
|
2054
|
+
...value,
|
|
2055
|
+
dnsRecords: {
|
|
2056
|
+
...fallback.dnsRecords,
|
|
2057
|
+
...(value.dnsRecords || {}),
|
|
2058
|
+
www: {
|
|
2059
|
+
...fallback.dnsRecords.www,
|
|
2060
|
+
...(value?.dnsRecords?.www || {}),
|
|
2061
|
+
},
|
|
2062
|
+
apex: {
|
|
2063
|
+
...fallback.dnsRecords.apex,
|
|
2064
|
+
...(value?.dnsRecords?.apex || {}),
|
|
2065
|
+
},
|
|
2066
|
+
},
|
|
2067
|
+
}
|
|
2068
|
+
}))
|
|
1920
2069
|
}
|
|
1921
2070
|
|
|
1922
|
-
return {
|
|
2071
|
+
return {
|
|
2072
|
+
project: CLOUDFLARE_PAGES_PROJECT || '',
|
|
2073
|
+
pagesDomain: pagesTarget,
|
|
2074
|
+
domainRegistry,
|
|
2075
|
+
}
|
|
1923
2076
|
})
|
|
1924
2077
|
|
|
1925
2078
|
exports.generateBlockFields = onCall({ timeoutSeconds: 180 }, async (request) => {
|
|
2079
|
+
assertCallableUser(request)
|
|
1926
2080
|
const data = request.data || {}
|
|
1927
2081
|
const auth = request.auth
|
|
1928
|
-
const { orgId,
|
|
1929
|
-
|
|
1930
|
-
if (!auth?.uid || auth.uid !== uid)
|
|
1931
|
-
throw new HttpsError('permission-denied', 'Unauthorized')
|
|
2082
|
+
const { orgId, blockId, blockName, content, fields, currentValues, meta, instructions } = data
|
|
1932
2083
|
if (!orgId || !blockId)
|
|
1933
2084
|
throw new HttpsError('invalid-argument', 'Missing orgId or blockId')
|
|
1934
2085
|
if (!Array.isArray(fields) || fields.length === 0)
|
|
@@ -2069,7 +2220,6 @@ exports.ensurePublishedSiteDomains = onDocumentWritten(
|
|
|
2069
2220
|
const siteRef = change.after.ref
|
|
2070
2221
|
const siteData = change.after.data() || {}
|
|
2071
2222
|
const beforeData = change.before?.data?.() || {}
|
|
2072
|
-
const domainErrorChanged = beforeData?.domainError !== siteData?.domainError
|
|
2073
2223
|
const rawDomains = Array.isArray(siteData.domains) ? siteData.domains : []
|
|
2074
2224
|
const normalizedDomains = Array.from(new Set(rawDomains.map(normalizeDomain).filter(Boolean)))
|
|
2075
2225
|
const beforeRawDomains = Array.isArray(beforeData.domains) ? beforeData.domains : []
|
|
@@ -2139,41 +2289,143 @@ exports.ensurePublishedSiteDomains = onDocumentWritten(
|
|
|
2139
2289
|
filteredDomains = normalizedDomains.filter(domain => !conflictSet.has(domain))
|
|
2140
2290
|
}
|
|
2141
2291
|
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2292
|
+
const pagesTarget = getCloudflarePagesTarget()
|
|
2293
|
+
const registryStateByDomain = new Map()
|
|
2294
|
+
const syncPlanMap = new Map()
|
|
2295
|
+
for (const domain of filteredDomains) {
|
|
2296
|
+
const dnsPayload = buildDomainDnsPayload(domain, pagesTarget)
|
|
2297
|
+
registryStateByDomain.set(domain, {
|
|
2298
|
+
...dnsPayload,
|
|
2299
|
+
wwwAdded: false,
|
|
2300
|
+
wwwError: '',
|
|
2301
|
+
apexAttempted: false,
|
|
2302
|
+
apexAdded: false,
|
|
2303
|
+
apexError: '',
|
|
2304
|
+
dnsGuidance: dnsPayload.dnsEligible
|
|
2305
|
+
? 'Add the www CNAME record. Apex is unavailable; forward apex to www.'
|
|
2306
|
+
: 'DNS records are not shown for localhost, IP addresses, or .dev domains.',
|
|
2307
|
+
})
|
|
2308
|
+
|
|
2309
|
+
const apexDomain = dnsPayload.apexDomain
|
|
2310
|
+
if (!apexDomain)
|
|
2311
|
+
continue
|
|
2312
|
+
const existingPlan = syncPlanMap.get(apexDomain) || {
|
|
2313
|
+
apexDomain,
|
|
2314
|
+
wwwDomain: dnsPayload.wwwDomain,
|
|
2315
|
+
domains: new Set(),
|
|
2316
|
+
}
|
|
2317
|
+
existingPlan.domains.add(domain)
|
|
2318
|
+
syncPlanMap.set(apexDomain, existingPlan)
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const syncPlans = Array.from(syncPlanMap.values())
|
|
2322
|
+
.filter(plan => shouldSyncCloudflareDomain(plan.wwwDomain))
|
|
2323
|
+
.map(plan => ({ ...plan, domains: Array.from(plan.domains) }))
|
|
2324
|
+
|
|
2147
2325
|
const removeDomains = Array.from(new Set(
|
|
2148
2326
|
removedOwnedDomains
|
|
2149
|
-
.
|
|
2327
|
+
.flatMap((domain) => {
|
|
2328
|
+
const apexDomain = getCloudflareApexDomain(domain)
|
|
2329
|
+
const wwwDomain = getCloudflarePagesDomain(apexDomain)
|
|
2330
|
+
return [wwwDomain, apexDomain]
|
|
2331
|
+
})
|
|
2150
2332
|
.filter(domain => shouldSyncCloudflareDomain(domain)),
|
|
2151
2333
|
))
|
|
2152
2334
|
if (removeDomains.length) {
|
|
2153
2335
|
await Promise.all(removeDomains.map(domain => removeCloudflarePagesDomain(domain, { orgId, siteId })))
|
|
2154
2336
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2337
|
+
|
|
2338
|
+
const syncResults = await Promise.all(syncPlans.map(async (plan) => {
|
|
2339
|
+
const wwwResult = await addCloudflarePagesDomain(plan.wwwDomain, { orgId, siteId, variant: 'www' })
|
|
2340
|
+
let apexAttempted = false
|
|
2341
|
+
let apexResult = { ok: false, error: '' }
|
|
2342
|
+
if (shouldSyncCloudflareDomain(plan.apexDomain)) {
|
|
2343
|
+
apexAttempted = true
|
|
2344
|
+
apexResult = await addCloudflarePagesDomain(plan.apexDomain, { orgId, siteId, variant: 'apex' })
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
...plan,
|
|
2348
|
+
apexAttempted,
|
|
2349
|
+
wwwResult,
|
|
2350
|
+
apexResult,
|
|
2351
|
+
}
|
|
2352
|
+
}))
|
|
2353
|
+
|
|
2354
|
+
for (const plan of syncResults) {
|
|
2355
|
+
const wwwAdded = !!plan.wwwResult?.ok
|
|
2356
|
+
const wwwError = wwwAdded ? '' : String(plan.wwwResult?.error || 'Failed to add www domain.')
|
|
2357
|
+
const apexAdded = !!plan.apexResult?.ok
|
|
2358
|
+
const apexError = apexAdded
|
|
2359
|
+
? ''
|
|
2360
|
+
: (plan.apexAttempted ? String(plan.apexResult?.error || 'Failed to add apex domain.') : '')
|
|
2361
|
+
|
|
2362
|
+
for (const domain of plan.domains) {
|
|
2363
|
+
const current = registryStateByDomain.get(domain) || buildDomainDnsPayload(domain, pagesTarget)
|
|
2364
|
+
const dnsGuidance = !current.dnsEligible
|
|
2365
|
+
? 'DNS records are not shown for localhost, IP addresses, or .dev domains.'
|
|
2366
|
+
: (apexAdded
|
|
2367
|
+
? 'Apex and www were added to Cloudflare Pages. Add both DNS records if your provider requires manual setup.'
|
|
2368
|
+
: 'Add the www CNAME record. Apex is unavailable; forward apex to www.')
|
|
2369
|
+
const nextDnsRecords = {
|
|
2370
|
+
...(current.dnsRecords || {}),
|
|
2371
|
+
apex: {
|
|
2372
|
+
...(current?.dnsRecords?.apex || {}),
|
|
2373
|
+
enabled: !!current.dnsEligible && !!current?.dnsRecords?.apex?.value && apexAdded,
|
|
2374
|
+
},
|
|
2375
|
+
www: {
|
|
2376
|
+
...(current?.dnsRecords?.www || {}),
|
|
2377
|
+
enabled: !!current.dnsEligible && !!current?.dnsRecords?.www?.value,
|
|
2378
|
+
},
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
registryStateByDomain.set(domain, {
|
|
2382
|
+
...current,
|
|
2383
|
+
dnsRecords: nextDnsRecords,
|
|
2384
|
+
wwwAdded,
|
|
2385
|
+
wwwError,
|
|
2386
|
+
apexAttempted: !!plan.apexAttempted,
|
|
2387
|
+
apexAdded,
|
|
2388
|
+
apexError,
|
|
2389
|
+
dnsGuidance,
|
|
2390
|
+
})
|
|
2158
2391
|
}
|
|
2159
|
-
return
|
|
2160
2392
|
}
|
|
2161
2393
|
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2394
|
+
if (registryStateByDomain.size) {
|
|
2395
|
+
for (const [domain, value] of registryStateByDomain.entries()) {
|
|
2396
|
+
const registryRef = db.collection(DOMAIN_REGISTRY_COLLECTION).doc(domain)
|
|
2397
|
+
const payload = {
|
|
2398
|
+
domain,
|
|
2399
|
+
orgId,
|
|
2400
|
+
siteId,
|
|
2401
|
+
sitePath: siteRef.path,
|
|
2402
|
+
updatedAt: Firestore.FieldValue.serverTimestamp(),
|
|
2403
|
+
apexDomain: value.apexDomain || '',
|
|
2404
|
+
wwwDomain: value.wwwDomain || '',
|
|
2405
|
+
dnsEligible: !!value.dnsEligible,
|
|
2406
|
+
apexAttempted: !!value.apexAttempted,
|
|
2407
|
+
apexAdded: !!value.apexAdded,
|
|
2408
|
+
wwwAdded: !!value.wwwAdded,
|
|
2409
|
+
dnsRecords: value.dnsRecords || {},
|
|
2410
|
+
dnsGuidance: value.dnsGuidance || '',
|
|
2411
|
+
}
|
|
2412
|
+
payload.apexError = value.apexError ? value.apexError : Firestore.FieldValue.delete()
|
|
2413
|
+
payload.wwwError = value.wwwError ? value.wwwError : Firestore.FieldValue.delete()
|
|
2414
|
+
await registryRef.set(payload, { merge: true })
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2166
2417
|
|
|
2418
|
+
const failed = syncResults.filter(item => !item.wwwResult?.ok)
|
|
2167
2419
|
if (!failed.length) {
|
|
2168
|
-
if (!conflictDomains.length && siteData.domainError
|
|
2420
|
+
if (!conflictDomains.length && siteData.domainError) {
|
|
2169
2421
|
await siteRef.set({ domainError: Firestore.FieldValue.delete() }, { merge: true })
|
|
2170
2422
|
}
|
|
2171
2423
|
return
|
|
2172
2424
|
}
|
|
2173
2425
|
|
|
2174
|
-
const errorDomains = failed.map(item => item.
|
|
2426
|
+
const errorDomains = failed.map(item => item.wwwDomain)
|
|
2175
2427
|
const errorDetails = failed
|
|
2176
|
-
.map(item => item.
|
|
2428
|
+
.map(item => item.wwwResult?.error)
|
|
2177
2429
|
.filter(Boolean)
|
|
2178
2430
|
.join('; ')
|
|
2179
2431
|
const cloudflareMessage = `Cloudflare domain sync failed for "${errorDomains.join(', ')}". ${errorDetails || 'Check function logs.'}`.trim()
|
package/src/edgeFirebase.js
CHANGED
|
@@ -14,6 +14,13 @@ function formatPhoneNumber(phone) {
|
|
|
14
14
|
return `+1${numericPhone}`
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const assertCallableUser = (request) => {
|
|
18
|
+
if (!request?.auth?.uid)
|
|
19
|
+
throw new HttpsError('unauthenticated', 'Authentication required.')
|
|
20
|
+
if (request?.data?.uid !== request.auth.uid)
|
|
21
|
+
throw new HttpsError('permission-denied', 'UID mismatch.')
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
exports.uploadDocumentDeleted = onDocumentDeleted(
|
|
18
25
|
{ document: 'organizations/{orgId}/files/{docId}', timeoutSeconds: 180 },
|
|
19
26
|
async (event) => {
|
|
@@ -34,21 +41,19 @@ exports.uploadDocumentDeleted = onDocumentDeleted(
|
|
|
34
41
|
)
|
|
35
42
|
|
|
36
43
|
exports.addUpdateFileDoc = onCall(async (request) => {
|
|
44
|
+
assertCallableUser(request)
|
|
37
45
|
const data = request.data
|
|
38
|
-
const auth = request.auth
|
|
39
46
|
let docId = data?.docId
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
docId = docRef.id
|
|
51
|
-
}
|
|
47
|
+
console.log(data)
|
|
48
|
+
const orgId = data.orgId
|
|
49
|
+
if (docId) {
|
|
50
|
+
const docRef = db.collection(`organizations/${orgId}/files`).doc(docId)
|
|
51
|
+
await docRef.set(data, { merge: true })
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const docRef = db.collection(`organizations/${orgId}/files`).doc()
|
|
55
|
+
await docRef.set(data)
|
|
56
|
+
docId = docRef.id
|
|
52
57
|
}
|
|
53
58
|
console.log(docId)
|
|
54
59
|
return { docId }
|
|
@@ -426,91 +431,87 @@ exports.initFirestore = onCall(async (request) => {
|
|
|
426
431
|
})
|
|
427
432
|
|
|
428
433
|
exports.removeNonRegisteredUser = onCall(async (request) => {
|
|
434
|
+
assertCallableUser(request)
|
|
429
435
|
const data = request.data
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
436
|
+
const stagedUser = await db.collection('staged-users').doc(data.docId).get()
|
|
437
|
+
if (stagedUser.exists) {
|
|
438
|
+
const stagedUserData = stagedUser.data()
|
|
439
|
+
|
|
440
|
+
const rolesExist = stagedUserData.roles && Object.keys(stagedUserData.roles).length !== 0
|
|
441
|
+
const specialPermissionsExist = stagedUserData.specialPermissions && Object.keys(stagedUserData.specialPermissions).length !== 0
|
|
442
|
+
const userIdExistsAndNotBlank = stagedUserData.userId && stagedUserData.userId !== ''
|
|
443
|
+
|
|
444
|
+
if (!rolesExist && !specialPermissionsExist && !userIdExistsAndNotBlank) {
|
|
445
|
+
await db.collection('staged-users').doc(data.docId).delete()
|
|
446
|
+
return { success: true, message: '' }
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
let message = ''
|
|
450
|
+
if (rolesExist && specialPermissionsExist) {
|
|
451
|
+
message = 'Cannot delete because the non-registered user still has roles and special permissions assigned.'
|
|
443
452
|
}
|
|
444
|
-
else {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
else if (specialPermissionsExist) {
|
|
453
|
-
message = 'Cannot delete because the non-registered user still has special permissions assigned.'
|
|
454
|
-
}
|
|
455
|
-
else if (userIdExistsAndNotBlank) {
|
|
456
|
-
message = 'Cannot delete because the user is registered.'
|
|
457
|
-
}
|
|
458
|
-
return { success: false, message }
|
|
453
|
+
else if (rolesExist) {
|
|
454
|
+
message = 'Cannot delete because the non-registered user still has roles assigned.'
|
|
455
|
+
}
|
|
456
|
+
else if (specialPermissionsExist) {
|
|
457
|
+
message = 'Cannot delete because the non-registered user still has special permissions assigned.'
|
|
458
|
+
}
|
|
459
|
+
else if (userIdExistsAndNotBlank) {
|
|
460
|
+
message = 'Cannot delete because the user is registered.'
|
|
459
461
|
}
|
|
462
|
+
return { success: false, message }
|
|
460
463
|
}
|
|
461
464
|
}
|
|
462
465
|
return { success: false, message: 'Non-registered user not found.' }
|
|
463
466
|
})
|
|
464
467
|
|
|
465
468
|
exports.currentUserRegister = onCall(async (request) => {
|
|
469
|
+
assertCallableUser(request)
|
|
466
470
|
const data = request.data
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
471
|
+
const stagedUser = await db.collection('staged-users').doc(data.registrationCode).get()
|
|
472
|
+
if (!stagedUser.exists) {
|
|
473
|
+
return { success: false, message: 'Registration code not found.' }
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
const stagedUserData = await stagedUser.data()
|
|
477
|
+
let process = false
|
|
478
|
+
if (stagedUserData.isTemplate) {
|
|
479
|
+
process = true
|
|
472
480
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const currentRoles = currentUserData.roles || {}
|
|
489
|
-
const currentUserCollectionPaths = currentUserData.collectionPaths || []
|
|
490
|
-
let newRole = {}
|
|
491
|
-
if (stagedUserData.subCreate && Object.keys(stagedUserData.subCreate).length !== 0 && stagedUserData.isTemplate) {
|
|
492
|
-
if (!data.dynamicDocumentFieldValue) {
|
|
493
|
-
return { success: false, message: 'Dynamic document field value is required.' }
|
|
494
|
-
}
|
|
495
|
-
const rootPath = stagedUserData.subCreate.rootPath
|
|
496
|
-
const newDoc = stagedUserData.subCreate.documentStructure
|
|
497
|
-
newDoc[stagedUserData.subCreate.dynamicDocumentField] = data.dynamicDocumentFieldValue
|
|
498
|
-
const addedDoc = await db.collection(rootPath).add(newDoc)
|
|
499
|
-
await db.collection(rootPath).doc(addedDoc.id).update({ docId: addedDoc.id })
|
|
500
|
-
newRole = { [`${rootPath}-${addedDoc.id}`]: { collectionPath: `${rootPath}-${addedDoc.id}`, role: stagedUserData.subCreate.role } }
|
|
481
|
+
if (!stagedUserData.isTemplate && stagedUserData.userId === '') {
|
|
482
|
+
process = true
|
|
483
|
+
}
|
|
484
|
+
if (!process) {
|
|
485
|
+
return { success: false, message: 'Registration code not valid.' }
|
|
486
|
+
}
|
|
487
|
+
const newRoles = stagedUserData.roles || {}
|
|
488
|
+
const currentUser = await db.collection('users').doc(data.uid).get()
|
|
489
|
+
const currentUserData = await currentUser.data()
|
|
490
|
+
const currentRoles = currentUserData.roles || {}
|
|
491
|
+
const currentUserCollectionPaths = currentUserData.collectionPaths || []
|
|
492
|
+
let newRole = {}
|
|
493
|
+
if (stagedUserData.subCreate && Object.keys(stagedUserData.subCreate).length !== 0 && stagedUserData.isTemplate) {
|
|
494
|
+
if (!data.dynamicDocumentFieldValue) {
|
|
495
|
+
return { success: false, message: 'Dynamic document field value is required.' }
|
|
501
496
|
}
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
497
|
+
const rootPath = stagedUserData.subCreate.rootPath
|
|
498
|
+
const newDoc = stagedUserData.subCreate.documentStructure
|
|
499
|
+
newDoc[stagedUserData.subCreate.dynamicDocumentField] = data.dynamicDocumentFieldValue
|
|
500
|
+
const addedDoc = await db.collection(rootPath).add(newDoc)
|
|
501
|
+
await db.collection(rootPath).doc(addedDoc.id).update({ docId: addedDoc.id })
|
|
502
|
+
newRole = { [`${rootPath}-${addedDoc.id}`]: { collectionPath: `${rootPath}-${addedDoc.id}`, role: stagedUserData.subCreate.role } }
|
|
503
|
+
}
|
|
504
|
+
const combinedRoles = { ...currentRoles, ...newRoles, ...newRole }
|
|
505
|
+
Object.values(combinedRoles).forEach((role) => {
|
|
506
|
+
if (!currentUserCollectionPaths.includes(role.collectionPath)) {
|
|
507
|
+
currentUserCollectionPaths.push(role.collectionPath)
|
|
511
508
|
}
|
|
512
|
-
|
|
509
|
+
})
|
|
510
|
+
await db.collection('staged-users').doc(currentUserData.stagedDocId).update({ roles: combinedRoles, collectionPaths: currentUserCollectionPaths })
|
|
511
|
+
if (!stagedUserData.isTemplate) {
|
|
512
|
+
await db.collection('staged-users').doc(data.registrationCode).delete()
|
|
513
513
|
}
|
|
514
|
+
return { success: true, message: '' }
|
|
514
515
|
}
|
|
515
516
|
})
|
|
516
517
|
|
|
@@ -522,52 +523,51 @@ exports.checkOrgIdExists = onCall(async (request) => {
|
|
|
522
523
|
})
|
|
523
524
|
|
|
524
525
|
exports.deleteSelf = onCall(async (request) => {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
526
|
+
assertCallableUser(request)
|
|
527
|
+
try {
|
|
528
|
+
const userDoc = await db.collection('staged-users').doc(request.auth.uid).get()
|
|
529
|
+
const userData = userDoc.data()
|
|
530
|
+
const userCollectionPaths = userData.collectionPaths || []
|
|
531
|
+
|
|
532
|
+
for (const path of userCollectionPaths) {
|
|
533
|
+
const usersWithSamePath = await db.collection('staged-users').where('collectionPaths', 'array-contains', path).get()
|
|
534
|
+
|
|
535
|
+
// If no other users have the same collection path, delete the path and all documents and collections under it
|
|
536
|
+
if (usersWithSamePath.size <= 1) {
|
|
537
|
+
const adjustedPath = path.replace(/-/g, '/')
|
|
538
|
+
const docRef = db.doc(adjustedPath)
|
|
539
|
+
const doc = await docRef.get()
|
|
540
|
+
|
|
541
|
+
if (doc.exists) {
|
|
542
|
+
// If the path is a document, delete it directly
|
|
543
|
+
await docRef.delete()
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
// If the path is a collection, delete all documents under it
|
|
547
|
+
const docsToDelete = await db.collection(adjustedPath).get()
|
|
548
|
+
const batch = db.batch()
|
|
549
|
+
docsToDelete.docs.forEach((doc) => {
|
|
550
|
+
batch.delete(doc.ref)
|
|
551
|
+
})
|
|
552
|
+
await batch.commit()
|
|
553
553
|
}
|
|
554
554
|
}
|
|
555
|
+
}
|
|
555
556
|
|
|
556
|
-
|
|
557
|
-
|
|
557
|
+
// Delete from 'staged-users' collection
|
|
558
|
+
await db.collection('staged-users').doc(request.data.uid).delete()
|
|
558
559
|
|
|
559
|
-
|
|
560
|
-
|
|
560
|
+
// Delete from 'users' collection
|
|
561
|
+
await db.collection('users').doc(request.data.uid).delete()
|
|
561
562
|
|
|
562
|
-
|
|
563
|
-
|
|
563
|
+
// Delete the user from Firebase
|
|
564
|
+
await admin.auth().deleteUser(request.data.uid)
|
|
564
565
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
566
|
+
return { success: true }
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
console.error('Error deleting user:', error)
|
|
570
|
+
return { success: false, error }
|
|
571
571
|
}
|
|
572
572
|
})
|
|
573
573
|
|