Files
donetick/src/utils/Helpers.jsx
2026-07-18 02:16:42 -04:00

87 lines
2.6 KiB
JavaScript

import moment from 'moment'
import { apiClient } from './ApiClient'
const isPlusAccount = userProfile => {
return userProfile?.expiration && moment(userProfile?.expiration).isAfter()
}
const resolvePhotoURL = url => {
if (!url) return ''
if (url.startsWith('http') || url.startsWith('https')) {
return url
}
return apiClient.getAssetURL(url)
}
// Returns the expiry of a signed asset URL in epoch ms, or null when the URL
// carries no expiry (public assets, plain paths, OIDC picture URLs).
// Understands the backend's local signer (`expires` in unix seconds) and
// S3 presigned URLs (`X-Amz-Date` + `X-Amz-Expires`).
const getSignedUrlExpiry = url => {
if (!url) return null
try {
const query = new URL(url, 'http://relative.local').searchParams
if (query.has('expires')) {
const expires = parseInt(query.get('expires'), 10)
return Number.isFinite(expires) ? expires * 1000 : null
}
if (query.has('X-Amz-Expires') && query.has('X-Amz-Date')) {
const iso = query
.get('X-Amz-Date')
.replace(
/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/,
'$1-$2-$3T$4:$5:$6Z',
)
const start = Date.parse(iso)
const validFor = parseInt(query.get('X-Amz-Expires'), 10)
if (Number.isFinite(start) && Number.isFinite(validFor)) {
return start + validFor * 1000
}
}
return null
} catch {
return null
}
}
// One minute of clock skew so we refresh before the server starts rejecting.
const isSignedUrlExpired = url => {
const expiry = getSignedUrlExpiry(url)
return expiry != null && Date.now() > expiry - 60_000
}
// Fetches a fresh signed URL for a stored asset path via the authed sign
// endpoint. Memoized per path until the signed URL nears expiry, with
// in-flight de-duplication so a burst of images signs each path once.
const signedUrlCache = new Map()
const getSignedAssetUrl = async path => {
if (!path) return ''
const hit = signedUrlCache.get(path)
if (hit?.url && !isSignedUrlExpired(hit.url)) return hit.url
if (hit?.promise) return hit.promise
const promise = (async () => {
const response = await apiClient.get(
`/files/sign?path=${encodeURIComponent(path)}`,
)
if (!response?.ok) {
throw new Error(`Failed to sign asset path: ${path}`)
}
const data = await response.json()
const url = resolvePhotoURL(data.url)
signedUrlCache.set(path, { url })
return url
})()
promise.catch(() => signedUrlCache.delete(path))
signedUrlCache.set(path, { promise })
return promise
}
export {
getSignedAssetUrl,
getSignedUrlExpiry,
isPlusAccount,
isSignedUrlExpired,
resolvePhotoURL,
}