bde148f286
* wip * some extra fixes * stuff kinda works? * need to figure out mocks * need to figure out mocks * fix db listener * gqlgen fix * minor gqlgen watch adjustment * lint fixes * delete old codegen file * converting migrations to ESM * getModuleDIrectory * vitest sort of works * added back ts-vitest * resolve gql double load * fixing test timeout configs * TSC lint fix * fix automate tests * moar debugging * debugging * more debugging * codegen update * server works * yargs migrated * chore(server): getting rid of global mocks for Server ESM (#5046) * got rid of email mock * got rid of comment mocks * got rid of multi region mocks * got rid of stripe mock * admin override mock updated * removed final mock * fixing import.meta.resolve calls * another import.meta.resolve fix * added requested test * nyc ESM fix * removed unneeded deps + linting * yarn lock forgot to commit * tryna fix flakyness * email capture util fix * sendEmail fix * fix TSX check * sender transporter fix + CR comments * merge main fix * test fixx * circleci fix * gqlgen bigint fix * error formatter fix * more error formatting improvements * esmloader added to Dockerfile * more dockerfile fixes * bg jobs fix
201 lines
6.2 KiB
TypeScript
201 lines
6.2 KiB
TypeScript
import type { GetFormattedObject } from '@/modules/core/domain/objects/operations'
|
|
import type { GetStream } from '@/modules/core/domain/streams/operations'
|
|
import type {
|
|
CheckStreamPermissions,
|
|
CreateObjectPreview,
|
|
GetObjectPreviewBufferOrFilepath,
|
|
GetObjectPreviewInfo,
|
|
GetPreviewImage,
|
|
SendObjectPreview
|
|
} from '@/modules/previews/domain/operations'
|
|
import { makeOgImage } from '@/modules/previews/ogImage'
|
|
import { authorizeResolver, validateScopes } from '@/modules/shared'
|
|
import { disablePreviews } from '@/modules/shared/helpers/envHelper'
|
|
import { Roles, Scopes } from '@speckle/shared'
|
|
import type { Logger } from 'pino'
|
|
import { PreviewPriority, PreviewStatus } from '@/modules/previews/domain/consts'
|
|
import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
const defaultAngle = '0'
|
|
|
|
export const getObjectPreviewBufferOrFilepathFactory =
|
|
(deps: {
|
|
getObject: GetFormattedObject
|
|
getObjectPreviewInfo: GetObjectPreviewInfo
|
|
createObjectPreview: CreateObjectPreview
|
|
getPreviewImage: GetPreviewImage
|
|
logger: Logger
|
|
}): GetObjectPreviewBufferOrFilepath =>
|
|
async ({ streamId, objectId, angle }) => {
|
|
const [noPreviewImage, previewErrorImage] = await Promise.all([
|
|
fileURLToPath(import.meta.resolve('#/assets/previews/images/no_preview.png')),
|
|
fileURLToPath(import.meta.resolve('#/assets/previews/images/preview_error.png'))
|
|
])
|
|
|
|
angle = angle || defaultAngle
|
|
const boundLogger = deps.logger.child({ streamId, objectId, angle })
|
|
|
|
if (disablePreviews()) {
|
|
return {
|
|
type: 'file',
|
|
file: noPreviewImage
|
|
}
|
|
}
|
|
|
|
// Check if objectId is valid
|
|
const dbObj = await deps.getObject({ streamId, objectId })
|
|
if (!dbObj) {
|
|
return {
|
|
type: 'file',
|
|
file: fileURLToPath(
|
|
import.meta.resolve('#/assets/previews/images/preview_404.png')
|
|
),
|
|
error: true,
|
|
errorCode: 'OBJECT_NOT_FOUND'
|
|
}
|
|
}
|
|
|
|
// Get existing preview metadata
|
|
const previewInfo = await deps.getObjectPreviewInfo({ streamId, objectId })
|
|
if (!previewInfo) {
|
|
const objPreviewQueued = await deps.createObjectPreview({
|
|
streamId,
|
|
objectId,
|
|
priority: PreviewPriority.LOW
|
|
})
|
|
if (!objPreviewQueued) return { type: 'file', file: noPreviewImage }
|
|
}
|
|
|
|
if (
|
|
!previewInfo ||
|
|
previewInfo.previewStatus !== PreviewStatus.DONE ||
|
|
!previewInfo.preview
|
|
) {
|
|
return { type: 'file', file: noPreviewImage }
|
|
}
|
|
|
|
const previewImgId = previewInfo.preview[angle]
|
|
if (!previewImgId) {
|
|
boundLogger.warn(
|
|
"Preview angle '{angle}' not found for object {streamId}:{objectId}"
|
|
)
|
|
return {
|
|
type: 'file',
|
|
error: true,
|
|
errorCode: 'ANGLE_NOT_FOUND',
|
|
file: previewErrorImage
|
|
}
|
|
}
|
|
const previewImg = await deps.getPreviewImage({ previewId: previewImgId })
|
|
if (!previewImg) {
|
|
boundLogger.warn(
|
|
{ previewImageId: previewImgId },
|
|
'Preview image not found: {previewImageId}'
|
|
)
|
|
return {
|
|
type: 'file',
|
|
file: previewErrorImage,
|
|
error: true,
|
|
errorCode: 'PREVIEW_NOT_FOUND'
|
|
}
|
|
}
|
|
return { type: 'buffer', buffer: previewImg }
|
|
}
|
|
|
|
export const sendObjectPreviewFactory =
|
|
(deps: {
|
|
getObjectPreviewBufferOrFilepath: GetObjectPreviewBufferOrFilepath
|
|
getStream: GetStream
|
|
makeOgImage: typeof makeOgImage
|
|
}): SendObjectPreview =>
|
|
async (req, res, streamId, objectId, angle) => {
|
|
let previewBufferOrFile = await deps.getObjectPreviewBufferOrFilepath({
|
|
streamId,
|
|
objectId,
|
|
angle
|
|
})
|
|
|
|
if (req.query.postprocess === 'og') {
|
|
const stream = await deps.getStream({ streamId: req.params.streamId })
|
|
const streamName = stream!.name
|
|
|
|
if (previewBufferOrFile.type === 'file') {
|
|
previewBufferOrFile = {
|
|
type: 'buffer',
|
|
buffer: await deps.makeOgImage(previewBufferOrFile.file, streamName)
|
|
}
|
|
} else {
|
|
previewBufferOrFile = {
|
|
type: 'buffer',
|
|
buffer: await deps.makeOgImage(previewBufferOrFile.buffer, streamName)
|
|
}
|
|
}
|
|
}
|
|
if (previewBufferOrFile.error) {
|
|
res.set('X-Preview-Error', 'true')
|
|
}
|
|
if (previewBufferOrFile.errorCode) {
|
|
res.set('X-Preview-Error-Code', previewBufferOrFile.errorCode)
|
|
}
|
|
if (previewBufferOrFile.type === 'file') {
|
|
// we can't cache these cause they may switch to proper buffer previews in a sec
|
|
// at least if they're not in the error state which they will not get out of (and thus can be cached in that scenario)
|
|
if (previewBufferOrFile.error) {
|
|
res.set('Cache-Control', 'private, max-age=604800')
|
|
} else {
|
|
res.set('Cache-Control', 'no-cache, no-store')
|
|
}
|
|
res.sendFile(previewBufferOrFile.file)
|
|
} else {
|
|
res.contentType('image/png')
|
|
// If the preview is a buffer, it comes from the DB and can be cached on clients
|
|
res.set('Cache-Control', 'private, max-age=604800')
|
|
res.send(previewBufferOrFile.buffer)
|
|
}
|
|
}
|
|
|
|
export const checkStreamPermissionsFactory =
|
|
(deps: {
|
|
validateScopes: typeof validateScopes
|
|
authorizeResolver: typeof authorizeResolver
|
|
getStream: GetStream
|
|
}): CheckStreamPermissions =>
|
|
async (req) => {
|
|
const stream = await deps.getStream({
|
|
streamId: req.params.streamId,
|
|
userId: req.context.userId
|
|
})
|
|
|
|
if (!stream) {
|
|
return { hasPermissions: false, httpErrorCode: 404 }
|
|
}
|
|
|
|
if (
|
|
stream.visibility !== ProjectRecordVisibility.Public &&
|
|
req.context.auth === false
|
|
) {
|
|
return { hasPermissions: false, httpErrorCode: 401 }
|
|
}
|
|
|
|
if (stream.visibility !== ProjectRecordVisibility.Public) {
|
|
try {
|
|
await deps.validateScopes(req.context.scopes, Scopes.Streams.Read)
|
|
} catch {
|
|
return { hasPermissions: false, httpErrorCode: 401 }
|
|
}
|
|
|
|
try {
|
|
await deps.authorizeResolver(
|
|
req.context.userId,
|
|
req.params.streamId,
|
|
Roles.Stream.Reviewer,
|
|
req.context.resourceAccessRules
|
|
)
|
|
} catch {
|
|
return { hasPermissions: false, httpErrorCode: 401 }
|
|
}
|
|
}
|
|
return { hasPermissions: true, httpErrorCode: 200 }
|
|
}
|