You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wiki/backend/api/analytics.ts

155 lines
4.6 KiB

import { audit } from '../helpers/audit.ts'
import type { FastifyInstance } from 'fastify'
import type { AnalyticsProviderInput } from '../models/analytics.ts'
/**
* Analytics API Routes
*/
async function routes(app: FastifyInstance) {
/**
* GET SITE ANALYTICS CONFIGURATION
*/
app.get<{ Params: { siteId: string } }>(
'/sites/:siteId/analytics',
{
config: {
permissions: ['manage:sites']
},
schema: {
summary: 'Get the analytics configuration of a site',
description:
'One entry per analytics module installed in `modules/analytics`, whether or not it has ever been enabled, each with the values this site has configured for it. Nothing is masked: every value here ends up in the HTML the site serves, so none of it can be a secret.',
tags: ['Analytics'],
params: {
type: 'object',
properties: {
siteId: {
type: 'string',
format: 'uuid'
}
},
required: ['siteId']
},
response: {
200: {
description: 'Analytics configuration of the site',
type: 'object',
properties: {
providers: {
type: 'array',
items: { $ref: 'AnalyticsProvider#' }
}
}
}
}
}
},
async (req, reply) => {
const site = await WIKI.models.sites.getSiteById({ id: req.params.siteId })
if (!site) {
return reply.notFound('Site does not exist.')
}
return { providers: WIKI.models.analytics.getSiteProviders(req.params.siteId) }
}
)
/**
* UPDATE SITE ANALYTICS CONFIGURATION
*/
app.put<{
Params: { siteId: string }
Body: { providers?: AnalyticsProviderInput[] }
}>(
'/sites/:siteId/analytics',
{
config: {
permissions: ['manage:sites']
},
schema: {
summary: 'Update the analytics configuration of a site',
description:
'Only the providers listed are affected, and within each of them only the props the module declares. Everything is validated before any of it is written, so a rejected request changes nothing. A saved change applies to the next document the site serves, on every instance.',
tags: ['Analytics'],
params: {
type: 'object',
properties: {
siteId: {
type: 'string',
format: 'uuid'
}
},
required: ['siteId']
},
body: {
type: 'object',
properties: {
providers: {
type: 'array',
items: { $ref: 'AnalyticsProviderInput#' }
}
}
},
response: {
200: {
description: 'Analytics configuration updated successfully',
type: 'object',
properties: {
ok: {
type: 'boolean'
},
message: {
type: 'string'
},
updated: {
type: 'integer',
description:
'How many providers were written. One already in the requested state still counts.'
}
}
}
}
}
},
async (req, reply) => {
const site = await WIKI.models.sites.getSiteById({ id: req.params.siteId })
if (!site) {
return reply.notFound('Site does not exist.')
}
// -> Validated as a whole first: the admin area saves every provider at once, and a partially
// applied configuration is worse than a refused one
const patches = req.body.providers ?? []
for (const patch of patches) {
const invalid = WIKI.models.analytics.validateProvider(patch)
if (invalid) {
return reply.badRequest(invalid)
}
}
const updated = await WIKI.models.analytics.updateSiteProviders(req.params.siteId, patches)
/*
Which providers were written and whether each was turned on — not the values, which are
identifiers of accounts at a third party rather than anything about this wiki. `meta` carries
identity and never payload.
*/
await audit(req, 'admin', 'updateAnalytics', {
siteId: req.params.siteId,
providers: patches.map((patch) => ({
key: patch.key,
isEnabled: patch.isEnabled,
changedFields: Object.keys(patch.config ?? {})
}))
})
return {
ok: true,
message: 'Analytics configuration updated successfully.',
updated
}
}
)
}
export default routes