mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-28 15:51:29 +08:00
fix(audit): enforce HTTP usecase boundary
Move transfer audit persistence and redirect audit target resolution behind usecase functions so HTTP handlers pass dependencies whole and satisfy the architectural boundary.
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
import type { Context } from 'hono'
|
||||
import { createMiddleware } from 'hono/factory'
|
||||
import { ZPAN_CLOUD_URL_DEFAULT } from '../../shared/constants'
|
||||
import { recordDownloadIssued, transferAuditActor } from '../middleware/audit-transfers'
|
||||
import { transferAuditActor } from '../middleware/audit-transfers'
|
||||
import { requireTeamRole } from '../middleware/auth'
|
||||
import type { Env } from '../middleware/platform'
|
||||
import {
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
updateObject,
|
||||
} from '../usecases/object'
|
||||
import { badRequest, forbidden, type Matter, unauthorized } from '../usecases/ports'
|
||||
import { recordDownloadIssued } from '../usecases/transfer-activity'
|
||||
import { errorResponse, jsonBody, jsonContent } from './openapi'
|
||||
|
||||
// The wire shape of a file/folder — exactly what the API serializes. Timestamps
|
||||
@@ -443,7 +444,7 @@ const objects = app
|
||||
if (result.ok) {
|
||||
if ('downloadUrl' in result) {
|
||||
await recordDownloadIssued(
|
||||
c.get('deps').audit,
|
||||
c.get('deps'),
|
||||
transferAuditActor(c.get('principal')),
|
||||
'object_download',
|
||||
{
|
||||
|
||||
+7
-53
@@ -1,14 +1,7 @@
|
||||
import type { Context } from 'hono'
|
||||
import { Hono } from 'hono'
|
||||
import { ZPAN_CLOUD_URL_DEFAULT } from '../../shared/constants'
|
||||
import {
|
||||
isDownloadFailureStatus,
|
||||
recordDownloadFailure,
|
||||
recordDownloadIssued,
|
||||
type TransferAuditTarget,
|
||||
transferAuditActor,
|
||||
transferFailureReason,
|
||||
} from '../middleware/audit-transfers'
|
||||
import { isDownloadFailureStatus, transferAuditActor, transferFailureReason } from '../middleware/audit-transfers'
|
||||
import type { Env } from '../middleware/platform'
|
||||
import { notFound } from '../usecases/ports'
|
||||
import {
|
||||
@@ -16,7 +9,9 @@ import {
|
||||
type ImageHostingOutcome,
|
||||
resolveDirectShareDownload,
|
||||
resolveImageHostingDownload,
|
||||
resolveRedirectDownloadAuditTarget,
|
||||
} from '../usecases/redirect'
|
||||
import { recordDownloadFailure, recordDownloadIssued } from '../usecases/transfer-activity'
|
||||
|
||||
// Strip optional file extension from token (e.g. "ih_aB3xK9.png" → "ih_aB3xK9")
|
||||
function stripExtension(token: string): string {
|
||||
@@ -39,7 +34,7 @@ async function handleDirectShare(c: Context<Env>, token: string): Promise<Respon
|
||||
})
|
||||
if (outcome.ok) {
|
||||
await recordDownloadIssued(
|
||||
c.get('deps').audit,
|
||||
c.get('deps'),
|
||||
transferAuditActor(c.get('principal')),
|
||||
'share_download',
|
||||
{
|
||||
@@ -71,7 +66,7 @@ async function handleImageHosting(c: Context<Env>, token: string): Promise<Respo
|
||||
})
|
||||
if (outcome.ok) {
|
||||
await recordDownloadIssued(
|
||||
c.get('deps').audit,
|
||||
c.get('deps'),
|
||||
transferAuditActor(c.get('principal')),
|
||||
'image_hosting_download',
|
||||
{
|
||||
@@ -93,15 +88,10 @@ async function handleImageHosting(c: Context<Env>, token: string): Promise<Respo
|
||||
const app = new Hono<Env>()
|
||||
|
||||
app.use('/:token', async (c, next) => {
|
||||
const target = await redirectDownloadAuditTarget(c, stripExtension(c.req.param('token')))
|
||||
const target = await resolveRedirectDownloadAuditTarget(c.get('deps'), stripExtension(c.req.param('token')))
|
||||
await next()
|
||||
if (!target || !isDownloadFailureStatus(c.res.status)) return
|
||||
await recordDownloadFailure(
|
||||
c.get('deps').audit,
|
||||
transferAuditActor(c.get('principal')),
|
||||
target,
|
||||
transferFailureReason(c),
|
||||
)
|
||||
await recordDownloadFailure(c.get('deps'), transferAuditActor(c.get('principal')), target, transferFailureReason(c))
|
||||
})
|
||||
|
||||
app.get('/:token', async (c) => {
|
||||
@@ -114,40 +104,4 @@ app.get('/:token', async (c) => {
|
||||
throw notFound()
|
||||
})
|
||||
|
||||
async function redirectDownloadAuditTarget(c: Context<Env>, token: string): Promise<TransferAuditTarget | null> {
|
||||
if (token.startsWith('ds_')) {
|
||||
const resolved = await c.get('deps').share.resolveByToken(token)
|
||||
if (resolved.status !== 'ok' || resolved.share.kind !== 'direct') return null
|
||||
return {
|
||||
orgId: resolved.share.orgId,
|
||||
targetType: 'share',
|
||||
targetId: resolved.share.id,
|
||||
targetName: resolved.matter.name,
|
||||
bytes: resolved.matter.size ?? 0,
|
||||
source: 'direct_share',
|
||||
metadata: {
|
||||
shareId: resolved.share.id,
|
||||
matterId: resolved.matter.id,
|
||||
storageId: resolved.matter.storageId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (token.startsWith('ih_')) {
|
||||
const resolved = await c.get('deps').imageHosting.resolveActiveByToken(token)
|
||||
if (!resolved) return null
|
||||
return {
|
||||
orgId: resolved.image.orgId,
|
||||
targetType: 'image',
|
||||
targetId: resolved.image.id,
|
||||
targetName: resolved.image.path,
|
||||
bytes: resolved.image.size,
|
||||
source: 'image_hosting',
|
||||
metadata: { imageId: resolved.image.id, storageId: resolved.image.storageId },
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default app
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getCookie, setCookie } from 'hono/cookie'
|
||||
import { ZPAN_CLOUD_URL_DEFAULT } from '../../shared/constants'
|
||||
import { pageSchema } from '../../shared/schemas'
|
||||
import { createShareRequestSchema, listSharesQuerySchema, saveShareRequestSchema } from '../../shared/schemas/share'
|
||||
import { recordDownloadIssued, transferAuditActor } from '../middleware/audit-transfers'
|
||||
import { transferAuditActor } from '../middleware/audit-transfers'
|
||||
import { requireAuth, requireTeamRole } from '../middleware/auth'
|
||||
import type { Env } from '../middleware/platform'
|
||||
import type { Matter, ShareListItem } from '../usecases/ports'
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
verifySharePassword,
|
||||
viewShare,
|
||||
} from '../usecases/share'
|
||||
import { recordDownloadIssued } from '../usecases/transfer-activity'
|
||||
import { errorResponse, jsonBody, jsonContent } from './openapi'
|
||||
import { cookieName, decodeChildRef, readUserId, viewCookieName } from './share-utils'
|
||||
|
||||
@@ -242,7 +243,7 @@ pub.get('/:token/objects/:ref', async (c) => {
|
||||
})
|
||||
if (out.ok) {
|
||||
await recordDownloadIssued(
|
||||
c.get('deps').audit,
|
||||
c.get('deps'),
|
||||
transferAuditActor(c.get('principal')),
|
||||
'share_download',
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Hono } from 'hono'
|
||||
import { recordAuditEffect } from '../../lib/audit'
|
||||
import type { Env } from '../../middleware/platform'
|
||||
import { requireFeature } from '../../middleware/require-feature'
|
||||
import type { RecordAuditEventInput } from '../../usecases/ports'
|
||||
import { processDeliveryWebhook } from '../../usecases/store/store'
|
||||
import { recordAuditEventOnce } from '../../usecases/transfer-activity'
|
||||
import { getCloudBaseUrl, parseJson, sha256Hex } from './helpers'
|
||||
|
||||
export const cloudStoreWebhooks = new Hono<Env>().use(requireFeature('quota_store')).post('/webhook', async (c) => {
|
||||
@@ -37,10 +37,11 @@ export const cloudStoreWebhooks = new Hono<Env>().use(requireFeature('quota_stor
|
||||
packageName: event.packageName ?? null,
|
||||
},
|
||||
}
|
||||
await recordAuditEffect(auditEvent.action, () =>
|
||||
c
|
||||
.get('deps')
|
||||
.audit.recordOnce(auditEvent, `cloud-store:${event.eventId}`, new Date(event.occurredAt ?? Date.now())),
|
||||
await recordAuditEventOnce(
|
||||
c.get('deps'),
|
||||
auditEvent,
|
||||
`cloud-store:${event.eventId}`,
|
||||
new Date(event.occurredAt ?? Date.now()),
|
||||
)
|
||||
return c.json({ success: true, duplicate: outcome.duplicate, eventId: outcome.eventId })
|
||||
})
|
||||
|
||||
+13
-15
@@ -19,17 +19,8 @@ import {
|
||||
workspaceEntry,
|
||||
xmlResponse,
|
||||
} from '../domain/webdav-xml'
|
||||
import { recordAuditEffect } from '../lib/audit'
|
||||
import { mapDomainError } from '../lib/http-errors'
|
||||
import {
|
||||
isDownloadFailureStatus,
|
||||
recordDownloadFailure,
|
||||
recordDownloadIssued,
|
||||
recordUploadResult,
|
||||
type TransferAuditTarget,
|
||||
transferAuditActor,
|
||||
transferFailureReason,
|
||||
} from '../middleware/audit-transfers'
|
||||
import { isDownloadFailureStatus, transferAuditActor, transferFailureReason } from '../middleware/audit-transfers'
|
||||
import type { Env } from '../middleware/platform'
|
||||
import {
|
||||
ApiKeyRateLimitError,
|
||||
@@ -39,6 +30,13 @@ import {
|
||||
WebDavPathError,
|
||||
type WebDavTarget,
|
||||
} from '../usecases/ports'
|
||||
import {
|
||||
recordAuditEvent,
|
||||
recordDownloadFailure,
|
||||
recordDownloadIssued,
|
||||
recordUploadResult,
|
||||
type TransferAuditTarget,
|
||||
} from '../usecases/transfer-activity'
|
||||
import {
|
||||
activeLocks,
|
||||
activeLocksForResources,
|
||||
@@ -638,28 +636,28 @@ app.use('*', async (c, next) => {
|
||||
if (c.req.method === 'GET' && isDownloadFailureStatus(c.res.status)) {
|
||||
const target = await webDavDownloadAuditTarget(c, userId)
|
||||
if (target) {
|
||||
await recordDownloadFailure(c.get('deps').audit, actor, target, transferFailureReason(c))
|
||||
await recordDownloadFailure(c.get('deps'), actor, target, transferFailureReason(c))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (preparedAction && c.res.status < 400) {
|
||||
const event = await resolveWebDavActionAudit(c, userId, preparedAction)
|
||||
if (event) await recordAuditEffect(event.action, () => c.get('deps').audit.record({ ...actor, ...event }))
|
||||
if (event) await recordAuditEvent(c.get('deps'), { ...actor, ...event })
|
||||
}
|
||||
|
||||
if (c.req.method !== 'PUT') return
|
||||
if (c.res.status === 201 || c.res.status === 204) {
|
||||
const target = await webDavUploadedTarget(c, userId)
|
||||
if (!target) throw new Error('transfer_audit_context_missing:webdav_upload')
|
||||
await recordUploadResult(c.get('deps').audit, actor, target)
|
||||
await recordUploadResult(c.get('deps'), actor, target)
|
||||
return
|
||||
}
|
||||
|
||||
if (!preparedPut || !isUploadFailureStatus(c.res.status)) return
|
||||
const bytes = exactRequestContentLength(c)
|
||||
if (bytes === null) return
|
||||
await recordUploadResult(c.get('deps').audit, actor, { ...preparedPut, bytes }, transferFailureReason(c))
|
||||
await recordUploadResult(c.get('deps'), actor, { ...preparedPut, bytes }, transferFailureReason(c))
|
||||
})
|
||||
|
||||
type WebDavActionAuditContext = {
|
||||
@@ -1089,7 +1087,7 @@ async function finishWebDavDownload(
|
||||
): Promise<void> {
|
||||
await recordWebDavDownloadIssued(c.get('deps'), params)
|
||||
await recordDownloadIssued(
|
||||
c.get('deps').audit,
|
||||
c.get('deps'),
|
||||
transferAuditActor(c.get('principal')),
|
||||
'webdav_download',
|
||||
{
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import { DirType } from '@shared/constants'
|
||||
import type { Context } from 'hono'
|
||||
import { recordAuditEffect } from '../lib/audit'
|
||||
import type { AuditRepo, Matter, RecordAuditEventInput } from '../usecases/ports'
|
||||
import type { Matter } from '../usecases/ports'
|
||||
import { decodeChildRef } from '../usecases/share'
|
||||
import type { TransferAuditTarget } from '../usecases/transfer-activity'
|
||||
import { type AuditActor, auditActor } from './audit-actor'
|
||||
import { type AuditRoute, type AuditRouteContext, auditRoute } from './audit-registry'
|
||||
import type { AuthPrincipal, Env } from './platform'
|
||||
|
||||
export type TransferAuditTarget = {
|
||||
orgId: string
|
||||
targetType: 'file' | 'image' | 'share'
|
||||
targetId?: string
|
||||
targetName: string
|
||||
bytes: number
|
||||
source: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type DownloadIssuedAction = 'share_download' | 'object_download' | 'image_hosting_download' | 'webdav_download'
|
||||
|
||||
const DOWNLOAD_FAILURE_STATUSES = new Set([402, 422, 500])
|
||||
const UPLOAD_FAILURE_STATUSES = new Set([404, 409, 422, 500, 502])
|
||||
|
||||
@@ -125,52 +113,6 @@ export function transferFailureReason(c: Context<Env>): string {
|
||||
return 'internal'
|
||||
}
|
||||
|
||||
export function recordUploadResult(
|
||||
audit: Pick<AuditRepo, 'record'>,
|
||||
actor: AuditActor,
|
||||
target: TransferAuditTarget,
|
||||
reason?: string,
|
||||
): Promise<void> {
|
||||
const event = buildUploadResultEvent(actor, target, reason)
|
||||
return recordAuditEffect(event.action, () => audit.record(event))
|
||||
}
|
||||
|
||||
export function recordDownloadFailure(
|
||||
audit: Pick<AuditRepo, 'record'>,
|
||||
actor: AuditActor,
|
||||
target: TransferAuditTarget,
|
||||
reason: string,
|
||||
): Promise<void> {
|
||||
const event = buildDownloadFailureEvent(actor, target, reason)
|
||||
return recordAuditEffect(event.action, () => audit.record(event))
|
||||
}
|
||||
|
||||
export function recordDownloadIssued(
|
||||
audit: Pick<AuditRepo, 'record'>,
|
||||
actor: AuditActor,
|
||||
action: DownloadIssuedAction,
|
||||
target: TransferAuditTarget,
|
||||
trafficEventId: string,
|
||||
): Promise<void> {
|
||||
const event: RecordAuditEventInput = {
|
||||
...actor,
|
||||
orgId: target.orgId,
|
||||
action,
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId,
|
||||
targetName: target.targetName,
|
||||
metadata: {
|
||||
...target.metadata,
|
||||
direction: 'download',
|
||||
status: 'issued',
|
||||
source: target.source,
|
||||
bytes: target.bytes,
|
||||
trafficEventId,
|
||||
},
|
||||
}
|
||||
return recordAuditEffect(event.action, () => audit.record(event))
|
||||
}
|
||||
|
||||
export function transferAuditActor(principal: AuthPrincipal | null): AuditActor {
|
||||
return auditActor(principal)
|
||||
}
|
||||
@@ -315,51 +257,6 @@ function transferTargetName(context: AuditRouteContext): string | undefined {
|
||||
return transferTarget(context)?.targetName
|
||||
}
|
||||
|
||||
function buildUploadResultEvent(
|
||||
actor: AuditActor,
|
||||
target: TransferAuditTarget,
|
||||
reason?: string,
|
||||
): RecordAuditEventInput {
|
||||
return {
|
||||
...actor,
|
||||
orgId: target.orgId,
|
||||
action: reason ? 'upload_failed' : 'upload_confirm',
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId,
|
||||
targetName: target.targetName,
|
||||
metadata: {
|
||||
...target.metadata,
|
||||
bytes: target.bytes,
|
||||
source: target.source,
|
||||
status: reason ? 'failed' : 'success',
|
||||
...(reason ? { reason } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function buildDownloadFailureEvent(
|
||||
actor: AuditActor,
|
||||
target: TransferAuditTarget,
|
||||
reason: string,
|
||||
): RecordAuditEventInput {
|
||||
return {
|
||||
...actor,
|
||||
orgId: target.orgId,
|
||||
action: 'download_failed',
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId,
|
||||
targetName: target.targetName,
|
||||
metadata: {
|
||||
...target.metadata,
|
||||
direction: 'download',
|
||||
status: 'failed',
|
||||
source: target.source,
|
||||
bytes: target.bytes,
|
||||
reason,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function stringField(value: Record<string, unknown>, field: string): string | undefined {
|
||||
return typeof value[field] === 'string' ? value[field] : undefined
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { reportTrafficForDownload } from '../http/store/traffic-metering'
|
||||
import type { Env } from '../middleware/platform'
|
||||
import { forbidden, notFound, quotaExceeded, storageNotFound } from '../usecases/ports'
|
||||
import { confirmDownloadTraffic, reverseDownloadTraffic } from '../usecases/store/traffic-metering'
|
||||
import { createTrafficEventId } from '../usecases/transfer-activity'
|
||||
import { recordDownloadFailure, recordDownloadIssued } from './audit-transfers'
|
||||
import { createTrafficEventId, recordDownloadFailure, recordDownloadIssued } from '../usecases/transfer-activity'
|
||||
|
||||
function stripPort(host: string): string {
|
||||
const lastColon = host.lastIndexOf(':')
|
||||
@@ -103,7 +102,7 @@ async function handleImageByPath(c: Context<Env>, orgId: string, virtualPath: st
|
||||
console.error('[image-hosting-domain] incrementAccessCount failed:', error)
|
||||
}
|
||||
await recordDownloadIssued(
|
||||
c.get('deps').audit,
|
||||
c.get('deps'),
|
||||
{ userId: null, actorType: 'anonymous', actorRef: null },
|
||||
'image_hosting_download',
|
||||
{
|
||||
@@ -128,7 +127,7 @@ function recordImageDownloadFailure(
|
||||
reason: string,
|
||||
): Promise<void> {
|
||||
return recordDownloadFailure(
|
||||
c.get('deps').audit,
|
||||
c.get('deps'),
|
||||
{ userId: null, actorType: 'anonymous', actorRef: null },
|
||||
{
|
||||
orgId: image.orgId,
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
reportDownloadEgress,
|
||||
reverseDownloadTraffic,
|
||||
} from './store/traffic-metering'
|
||||
import { createTrafficEventId } from './transfer-activity'
|
||||
import { createTrafficEventId, type TransferAuditTarget } from './transfer-activity'
|
||||
|
||||
// The metering usecases need the cloud-report ports plus quota; the redirect
|
||||
// flows additionally read shares / image-hosting / storages and presign via s3.
|
||||
@@ -42,6 +42,45 @@ export type RedirectDeps = CloudTrafficMeteringDeps & {
|
||||
imageHosting: ImageHostingRepo
|
||||
}
|
||||
|
||||
export async function resolveRedirectDownloadAuditTarget(
|
||||
deps: Pick<RedirectDeps, 'share' | 'imageHosting'>,
|
||||
token: string,
|
||||
): Promise<TransferAuditTarget | null> {
|
||||
if (token.startsWith('ds_')) {
|
||||
const resolved = await deps.share.resolveByToken(token)
|
||||
if (resolved.status !== 'ok' || resolved.share.kind !== 'direct') return null
|
||||
return {
|
||||
orgId: resolved.share.orgId,
|
||||
targetType: 'share',
|
||||
targetId: resolved.share.id,
|
||||
targetName: resolved.matter.name,
|
||||
bytes: resolved.matter.size ?? 0,
|
||||
source: 'direct_share',
|
||||
metadata: {
|
||||
shareId: resolved.share.id,
|
||||
matterId: resolved.matter.id,
|
||||
storageId: resolved.matter.storageId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (token.startsWith('ih_')) {
|
||||
const resolved = await deps.imageHosting.resolveActiveByToken(token)
|
||||
if (!resolved) return null
|
||||
return {
|
||||
orgId: resolved.image.orgId,
|
||||
targetType: 'image',
|
||||
targetId: resolved.image.id,
|
||||
targetName: resolved.image.path,
|
||||
bytes: resolved.image.size,
|
||||
source: 'image_hosting',
|
||||
metadata: { imageId: resolved.image.id, storageId: resolved.image.storageId },
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// ─── Direct share (ds_) ──────────────────────────────────────────────────────
|
||||
|
||||
export type DirectShareOutcome =
|
||||
|
||||
@@ -1,3 +1,106 @@
|
||||
import { recordAuditEffect } from '../lib/audit'
|
||||
import type { Deps } from './deps'
|
||||
import type { RecordAuditEventInput } from './ports'
|
||||
|
||||
export type TransferAuditTarget = {
|
||||
orgId: string
|
||||
targetType: 'file' | 'image' | 'share'
|
||||
targetId?: string
|
||||
targetName: string
|
||||
bytes: number
|
||||
source: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type TransferAuditActor = Pick<RecordAuditEventInput, 'userId' | 'actorType' | 'actorRef'>
|
||||
type DownloadIssuedAction = 'share_download' | 'object_download' | 'image_hosting_download' | 'webdav_download'
|
||||
|
||||
export function createTrafficEventId(): string {
|
||||
return `traffic_${crypto.randomUUID()}`
|
||||
}
|
||||
|
||||
export function recordAuditEvent(deps: Pick<Deps, 'audit'>, event: RecordAuditEventInput): Promise<void> {
|
||||
return recordAuditEffect(event.action, () => deps.audit.record(event))
|
||||
}
|
||||
|
||||
export function recordAuditEventOnce(
|
||||
deps: Pick<Deps, 'audit'>,
|
||||
event: RecordAuditEventInput,
|
||||
idempotencyKey: string,
|
||||
occurredAt: Date,
|
||||
): Promise<void> {
|
||||
return recordAuditEffect(event.action, () => deps.audit.recordOnce(event, idempotencyKey, occurredAt))
|
||||
}
|
||||
|
||||
export function recordUploadResult(
|
||||
deps: Pick<Deps, 'audit'>,
|
||||
actor: TransferAuditActor,
|
||||
target: TransferAuditTarget,
|
||||
reason?: string,
|
||||
): Promise<void> {
|
||||
const event: RecordAuditEventInput = {
|
||||
...actor,
|
||||
orgId: target.orgId,
|
||||
action: reason ? 'upload_failed' : 'upload_confirm',
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId,
|
||||
targetName: target.targetName,
|
||||
metadata: {
|
||||
...target.metadata,
|
||||
bytes: target.bytes,
|
||||
source: target.source,
|
||||
status: reason ? 'failed' : 'success',
|
||||
...(reason ? { reason } : {}),
|
||||
},
|
||||
}
|
||||
return recordAuditEvent(deps, event)
|
||||
}
|
||||
|
||||
export function recordDownloadFailure(
|
||||
deps: Pick<Deps, 'audit'>,
|
||||
actor: TransferAuditActor,
|
||||
target: TransferAuditTarget,
|
||||
reason: string,
|
||||
): Promise<void> {
|
||||
return recordAuditEvent(deps, {
|
||||
...actor,
|
||||
orgId: target.orgId,
|
||||
action: 'download_failed',
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId,
|
||||
targetName: target.targetName,
|
||||
metadata: {
|
||||
...target.metadata,
|
||||
direction: 'download',
|
||||
status: 'failed',
|
||||
source: target.source,
|
||||
bytes: target.bytes,
|
||||
reason,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function recordDownloadIssued(
|
||||
deps: Pick<Deps, 'audit'>,
|
||||
actor: TransferAuditActor,
|
||||
action: DownloadIssuedAction,
|
||||
target: TransferAuditTarget,
|
||||
trafficEventId: string,
|
||||
): Promise<void> {
|
||||
return recordAuditEvent(deps, {
|
||||
...actor,
|
||||
orgId: target.orgId,
|
||||
action,
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId,
|
||||
targetName: target.targetName,
|
||||
metadata: {
|
||||
...target.metadata,
|
||||
direction: 'download',
|
||||
status: 'issued',
|
||||
source: target.source,
|
||||
bytes: target.bytes,
|
||||
trafficEventId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user