feat(fe2): brave/safari cookie max-age workaround (#2211)

* feat(fe2): brave/safari cookie max-age workaround

* minor adjustments

* removed brave note
This commit is contained in:
Kristaps Fabians Geikins
2024-04-17 13:13:56 +03:00
committed by GitHub
parent 0b6684e485
commit 4e9c9eba48
4 changed files with 130 additions and 5 deletions
@@ -1,16 +1,34 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import type { CookieOptions } from 'nuxt/dist/app/composables/cookie'
import dayjs from 'dayjs'
import { useScopedState } from '~~/lib/common/composables/scopedState'
import { isUndefined } from 'lodash-es'
import { get, isUndefined } from 'lodash-es'
import { isBraveOrSafari, type Nullable } from '@speckle/shared'
class AbortControllerManager {
private abortController: Nullable<AbortController> = null
getAndAbortOld() {
if (process.server) return null
// Abort old
if (this.abortController) this.abortController.abort()
this.abortController = null
// Create new
this.abortController = new AbortController()
return this.abortController
}
}
const abortControllerManager = new AbortControllerManager()
/**
* Makes useCookie() synchronized across the app so that a change to it from one place
* will also update other references elsewhere.
*
* Defaults to an expiration date of 1 year
*
* IMPORTANT NOTE: Both Safari & Brave limit client-side cookie max-age to 7 days. If your cookie is important, evaluate how to
* ensure that the cookie is written to from the server-side (either SSR render or API route)
*/
export const useSynchronizedCookie = <CookieValue = string>(
name: string,
@@ -25,9 +43,34 @@ export const useSynchronizedCookie = <CookieValue = string>(
}
// something's off with nuxt's types here, have to use any
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
const cookie = useCookie<CookieValue>(name, finalOpts as any)
// Hack to resolve Safari & Brave limiting client-side cookies to 7 days - set temporary cookie to be read from server-side where it'll be fixed
if (process.client && isBraveOrSafari()) {
const tmpCookie = useCookie(`tmp-${name}`, finalOpts as any)
watch(cookie, (newVal) => {
if (newVal) {
tmpCookie.value = JSON.stringify({
expires: finalOpts.expires?.toISOString(),
maxAge: finalOpts.maxAge
})
} else {
tmpCookie.value = undefined
}
// Fetch w/ abort of previous call, if any
const controller = abortControllerManager.getAndAbortOld()
void fetch('/web-api/cookie-fix', {
signal: controller?.signal
}).catch((e) => {
if (get(e, 'name') !== 'AbortError') {
throw e
}
})
})
}
// there's a bug in nuxt where a default value doesn't get set if useCookie is only invoked in CSR
// TODO: https://github.com/nuxt/nuxt/issues/26701
if (isUndefined(cookie.value) && opts?.default) {
+9
View File
@@ -7,6 +7,7 @@ import { useCreateErrorLoggingTransport } from '~/lib/core/composables/error'
import type { Plugin } from 'nuxt/dist/app/nuxt'
import { isH3Error } from '~/lib/common/helpers/error'
import { useRequestId, useServerRequestId } from '~/lib/core/composables/server'
import { isBrave, isSafari } from '@speckle/shared'
type PluginNuxtApp = Parameters<Plugin>[0]
@@ -84,6 +85,14 @@ async function initRumClient(app: PluginNuxtApp) {
datadog.onReady(async () => {
if ('setGlobalContextProperty' in datadog && reqId?.length) {
datadog.setGlobalContextProperty('requestId', reqId)
if (isSafari()) {
datadog.setGlobalContextProperty('isSafari', 'true')
}
if (isBrave()) {
datadog.setGlobalContextProperty('isBrave', 'true')
}
}
await onAuthStateChange(
@@ -0,0 +1,56 @@
import type { Optional } from '@speckle/shared'
import { has, isObjectLike } from 'lodash-es'
type TempCookieValue = { expires?: Optional<string>; maxAge?: Optional<number> }
const isValidTempCookieValue = (cookie: unknown): cookie is TempCookieValue => {
if (!isObjectLike(cookie)) return false
return has(cookie, 'expires') || has(cookie, 'maxAge')
}
export default defineEventHandler((event) => {
const cookies = parseCookies(event)
const {
public: { baseUrl }
} = useRuntimeConfig()
const domain = new URL(baseUrl).hostname
for (const [key, val] of Object.entries(cookies)) {
if (key.startsWith('tmp-')) {
// Try reading in cookie settings
let tempCookieVal: Optional<TempCookieValue> = undefined
try {
const parsedVal = JSON.parse(val) as unknown
if (isValidTempCookieValue(parsedVal)) {
tempCookieVal = parsedVal
}
} catch (e) {
deleteCookie(event, key)
continue
}
if (!tempCookieVal) {
deleteCookie(event, key)
continue
}
// Try finding cookie that we need to fix
const cookieName = key.replace('tmp-', '')
const cookieValue = cookies[cookieName]
if (!cookieValue) {
deleteCookie(event, key)
continue
}
// Create new cookie with the correct settings
setCookie(event, cookieName, cookieValue, {
maxAge: tempCookieVal.maxAge,
expires: tempCookieVal.expires ? new Date(tempCookieVal.expires) : undefined,
domain
})
deleteCookie(event, key)
}
}
return { status: 'ok' }
})
+17
View File
@@ -1,3 +1,4 @@
import { get } from 'lodash'
import type { Nullable } from './utilityTypes'
export enum OperatingSystem {
@@ -85,3 +86,19 @@ export function isSafari() {
const userAgent = globalThis.navigator.userAgent
return /^((?!chrome|android).)*safari/i.test(userAgent)
}
/**
* Check if user is in Brave browser
*/
export function isBrave() {
if (!globalThis || !globalThis.navigator || !('brave' in globalThis.navigator)) {
return false
}
const braveObj = get(globalThis.navigator, 'brave')
if (!braveObj) return false
return !!get(braveObj, 'isBrave', false)
}
export const isBraveOrSafari = () => isBrave() || isSafari()