@flowfuse/flowfuse
Version:
An open source low-code development platform
922 lines (878 loc) • 37.2 kB
JavaScript
const crypto = require('crypto')
const { Op } = require('sequelize')
const { Roles } = require('../../lib/roles')
const teamShared = require('./shared/team.js')
const TeamDevices = require('./teamDevices.js')
const TeamInvitations = require('./teamInvitations.js')
const TeamMembers = require('./teamMembers.js')
/**
* Team api routes
*
* - /api/v1/teams
*
* - Any route that has a :teamId parameter will:
* - Ensure the session user is either admin or has a role on the team
* - request.team prepopulated with the team object
* - request.teamMembership prepopulated with the user role ({role: "member"})
* (unless they are admin)
*
*/
module.exports = async function (app) {
app.addHook('preHandler', teamShared.defaultPreHandler.bind(null, app))
app.post('/check-slug', {
preHandler: app.needsPermission('team:create'),
schema: {
summary: 'Check a team slug is available',
tags: ['Teams'],
body: {
type: 'object',
required: ['slug'],
properties: {
slug: { type: 'string' }
}
},
response: {
200: {
description: 'Team slug is available',
$ref: 'APIStatus'
},
409: {
description: 'Team slug is not available',
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
const slug = request.body.slug.toLowerCase()
const reservedNames = ['create']
if (reservedNames.includes(slug)) {
reply.code(409).send({ code: 'invalid_slug', error: 'Slug not available' })
return
}
const existingCount = await app.db.models.Team.count({ where: { slug } })
if (existingCount === 0) {
reply.send({ status: 'okay' })
} else {
reply.code(409).send({ code: 'invalid_slug', error: 'Slug not available' })
}
})
async function appendBillingDetails (result, team, request) {
if (app.license.active() && app.billing) {
result.billing = {}
const subscription = await team.getSubscription()
if (subscription) {
result.billing.active = subscription.isActive()
result.billing.unmanaged = subscription.isUnmanaged()
result.billing.canceled = subscription.isCanceled()
result.billing.pastDue = subscription.isPastDue()
if (subscription.isTrial()) {
result.billing.trial = true
result.billing.trialEnded = subscription.isTrialEnded()
result.billing.trialEndsAt = subscription.trialEndsAt
result.billing.trialProjectAllowed = (await team.instanceCount(app.settings.get('user:team:trial-mode:projectType'))) === 0
}
if (request.session.User.admin) {
result.billing.customer = subscription.customer
result.billing.subscription = subscription.subscription
}
} else {
result.billing.active = false
}
}
}
async function getTeamDetails (request, reply, team) {
if (!request.session.User?.admin && request.teamMembership.role < Roles.Viewer) {
// Return summary details for any role less than Viewer (eg dashboard)
reply.send(app.db.views.Team.teamSummary(team))
return
}
const result = app.db.views.Team.team(team)
result.instanceCountByType = await team.instanceCountByType()
await appendBillingDetails(result, team, request)
reply.send(result)
}
app.register(TeamMembers, { prefix: '/:teamId/members' })
app.register(TeamInvitations, { prefix: '/:teamId/invitations' })
app.register(TeamDevices, { prefix: '/:teamId/devices' })
/**
* Get the details of a team
* /api/v1/teams/:teamId
*/
app.get('/:teamId', {
preHandler: app.needsPermission('team:read'),
schema: {
summary: 'Get details of a team',
tags: ['Teams'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
$ref: 'Team'
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
await getTeamDetails(request, reply, request.team)
})
/**
* Get the details of a team via its slug
*
* /api/v1/teams/slug/:teamSlug
*/
app.get('/slug/:teamSlug', {
preHandler: app.needsPermission('team:read'),
schema: {
summary: 'Get details of a team using its slug',
tags: ['Teams'],
params: {
type: 'object',
properties: {
teamSlug: { type: 'string' }
}
},
response: {
200: {
$ref: 'Team'
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
await getTeamDetails(request, reply, request.team)
})
/**
* Get a list of all teams (admin-only)
*/
app.get('/', {
preHandler: app.needsPermission('team:list'),
schema: {
summary: 'Get a list of all teams - admin-only',
tags: ['Teams'],
query: { $ref: 'PaginationParams' },
response: {
200: {
type: 'object',
properties: {
meta: { $ref: 'PaginationMeta' },
count: { type: 'number' },
teams: { type: 'array', items: { $ref: 'Team' } }
}
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
// Admin request for all teams
const where = {}
const filters = []
if (request.query.teamType) {
const teamTypes = request.query.teamType.split(',').map(app.db.models.TeamType.decodeHashid).flat()
filters.push({ TeamTypeId: { [Op.in]: teamTypes } })
}
if (request.query.state === 'suspended') {
filters.push({ suspended: true })
} else if (app.billing && request.query.billing) {
filters.push({ suspended: false })
const billingStates = request.query.billing.split(',')
filters.push({ '$Subscription.status$': { [Op.in]: billingStates } })
}
if (filters.length > 0) {
where[Op.and] = filters
}
const paginationOptions = app.getPaginationOptions(request)
const teams = await app.db.models.Team.getAll(paginationOptions, where)
teams.teams = teams.teams.map(t => app.db.views.Team.team(t))
reply.send(teams)
})
/**
* Get a list of the teams applications
* /api/v1/teams/:teamId/applications
*/
app.get('/:teamId/applications', {
preHandler: app.needsPermission('team:projects:list'), // TODO Using project level permissions
schema: {
summary: 'Get a list of the teams applications',
tags: ['Teams'],
query: {
associationsLimit: { type: 'number' }
},
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
// meta: { $ref: 'PaginationMeta' },
count: { type: 'number' },
applications: { $ref: 'TeamApplicationList' }
}
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
const includeInstances = true
const includeApplicationDevices = true
const associationsLimit = request.query.associationsLimit
const includeApplicationSummary = !!associationsLimit || request.query.includeApplicationSummary
const applications = await app.db.models.Application.byTeam(request.params.teamId, { includeInstances, includeApplicationDevices, associationsLimit, includeApplicationSummary })
reply.send({
count: applications.length,
applications: await app.db.views.Application.teamApplicationList(applications, { includeInstances, includeApplicationDevices, includeApplicationSummary })
})
})
/**
* List team application associations (devices and instances) statuses
* @name /api/v1/teams:teamId/applications/status
* @memberof forge.routes.api.application
*/
app.get('/:teamId/applications/status', {
preHandler: app.needsPermission('team:projects:list'), // TODO Using project level permissions
schema: {
summary: 'Get a list of the teams applications statuses',
tags: ['Teams'],
query: {
associationsLimit: { type: 'number' }
},
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
// meta: { $ref: 'PaginationMeta' },
count: { type: 'number' },
applications: { $ref: 'ApplicationAssociationsStatusList' }
}
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
const includeInstances = true
const includeApplicationDevices = true
const associationsLimit = request.query.associationsLimit
const applications = await app.db.models.Application.byTeam(request.params.teamId, { includeInstances, includeApplicationDevices, includeInstanceStorageFlow: true, associationsLimit })
if (!applications) {
return reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
const applicationsWithAssociationsStatuses = await app.db.views.Application.applicationAssociationsStatusList(applications)
reply.send({
count: applicationsWithAssociationsStatuses.length,
applications: applicationsWithAssociationsStatuses
})
})
/**
* @deprecated Use /:teamId/applications, or /:applicationId/instances
* This end-point is still used by:
* - the project nodes and nr-tools plugin.
* - the Team Instances view
* - team/Devices/dialogs/CreateProvisionTokenDialog.vue
* - team/Settings/Devices.vue
*/
app.get('/:teamId/projects', {
preHandler: app.needsPermission('team:projects:list')
}, async (request, reply) => {
const projects = await app.db.models.Project.byTeam(request.params.teamId, { includeSettings: true })
if (projects) {
let result = await app.db.views.Project.instancesList(projects, { includeSettings: true })
if (request.session.ownerType === 'project') {
// This request is from a project token. Filter the list to return
// the minimal information needed
result = result.map(e => {
return { id: e.id, name: e.name }
})
}
reply.send({
count: result.length,
projects: result
})
} else {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
})
/**
* Get team instances that have dashboards installed
* /api/v1/teams/:teamId/dashboard-instances
*/
app.get('/:teamId/dashboard-instances', {
preHandler: app.needsPermission('team:read')
}, async (request, reply) => {
const projects = await app.db.models.Project.byTeamForDashboard(request.params.teamId)
if (projects && projects.length > 0) {
// filters out projects/instances without dashboards
const filtered = projects.filter(project => {
return project.ProjectSettings.filter(settingEntry => {
const isSettingsEntry = settingEntry.key === 'settings'
let hasDashboardInstalled = false
if (
isSettingsEntry &&
Object.prototype.hasOwnProperty.call(settingEntry.value, 'palette') &&
Object.prototype.hasOwnProperty.call(settingEntry.value.palette, 'modules')
) {
hasDashboardInstalled = !!settingEntry.value.palette.modules.find(module => module.name === '@flowfuse/node-red-dashboard')
}
return isSettingsEntry && hasDashboardInstalled
}).length > 0
})
if (filtered.length === 0) {
return reply.send({
count: 0,
projects: []
})
}
// map additional data
await Promise.all(filtered.map(async project => {
const projectStatePromise = project.liveState()
const projectState = await projectStatePromise
project.state = projectState.meta.state
project.flowLastUpdatedAt = projectState.flowLastUpdatedAt
project.settings = {
dashboard2UI: '/dashboard' // hardcoding the dashboard endpoint for the time being
}
}))
const result = await app.db.views.Project.dashboardInstancesSummaryList(filtered)
return reply.send({
count: result.length,
projects: result
})
} else {
return reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
})
async function createTeamApplication (user, team) {
const applicationName = `${user.name}'s Application`
const application = await app.db.models.Application.create({
name: applicationName.charAt(0).toUpperCase() + applicationName.slice(1),
TeamId: team.id
})
await app.auditLog.Team.application.created(user, null, team, application)
await app.auditLog.Application.application.created(user, null, application)
return application
}
/**
* Create a new team
* /api/v1/teams
*/
app.post('/', {
preHandler: app.needsPermission('team:create'),
schema: {
summary: 'Create a new team',
tags: ['Teams'],
body: {
type: 'object',
required: ['name', 'type'],
properties: {
name: { type: 'string' },
type: { type: 'string' },
slug: { type: 'string' },
trial: { type: 'boolean' }
}
},
response: {
200: {
$ref: 'Team'
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
if (!request.session.User.admin && !app.settings.get('team:create')) {
// Ideally this would be handled by `needsPermission`
// preHandler. To do so will require the perms model to know
// to also check enabled features (and know that admin is allowed to
// override in this instance)
reply.code(403).send({ code: 'unauthorized', error: 'unauthorized' })
}
// TODO check license allows multiple teams
if (request.body.slug === 'create') {
reply.code(400).send({ code: 'invalid_slug', error: 'slug not available' })
return
}
const teamType = await app.db.models.TeamType.byId(request.body.type)
if (!teamType || !teamType.active) {
reply.code(400).send({ code: 'invalid_team_type', error: 'unknown team type' })
return
}
let trialMode = false
if (app.license.active() && app.billing && request.body.trial) {
// Check this user is allowed to create a trial team of this type.
// Rules:
// 1. teamType must have trial mode enabled
const teamTrialActive = await teamType.getProperty('trial.active', false)
if (!teamTrialActive) {
reply.code(400).send({ code: 'invalid_request', error: 'trial mode not available' })
return
}
// 2. user must have no existing teams
const existingTeamCount = await app.db.models.Team.countForUser(request.session.User)
if (existingTeamCount > 0) {
reply.code(400).send({ code: 'invalid_request', error: 'trial mode not available' })
return
}
// 3. user must be < 1 week old
const delta = Date.now() - request.session.User.createdAt.getTime()
if (delta > 1000 * 60 * 60 * 24 * 7) {
reply.code(400).send({ code: 'invalid_request', error: 'trial mode not available' })
return
}
trialMode = true
} else if (request.body.trial) {
reply.code(400).send({ code: 'invalid_request', error: 'trial mode not available' })
return
}
let team
try {
team = await app.db.controllers.Team.createTeamForUser({
name: request.body.name,
slug: request.body.slug,
TeamTypeId: teamType.id
}, request.session.User)
await app.auditLog.Platform.platform.team.created(request.session.User, null, team)
await app.auditLog.Team.team.created(request.session.User, null, team)
await app.auditLog.Team.team.user.added(request.session.User, null, team, request.session.User)
const teamView = app.db.views.Team.team(team)
let defaultTeamCreated = false
if (app.license.active() && app.billing) {
if (trialMode) {
await app.billing.setupTrialTeamSubscription(team, request.session.User)
// In trial mode, we may also auto-create their first application and instance
if (app.settings.get('user:team:auto-create:instanceType')) {
const instanceTypeId = app.settings.get('user:team:auto-create:instanceType')
const instanceType = await app.db.models.ProjectType.byId(instanceTypeId)
const instanceStack = await instanceType?.getDefaultStack() || (await instanceType.getProjectStacks())?.[0]
const instanceTemplate = await app.db.models.ProjectTemplate.findOne({ where: { active: true } })
if (!instanceType) {
app.log.warn(`Unable to create Trial Instance in team ${team.hashid}: Instance type with id ${instanceTypeId} from 'user:team:auto-create:instanceType' not found`)
} else if (!instanceStack) {
app.log.warn(`Unable to create Trial Instance in team ${team.hashid}: Unable to find a stack for use with instance type ${instanceTypeId}`)
} else if (!instanceTemplate) {
app.log.warn(`Unable to create Trial Instance in team ${team.hashid}: Unable to find the default instance template`)
} else {
const safeTeamName = team.name.toLowerCase().replace(/[\W_]/g, '-')
const safeUserName = request.session.User.username.toLowerCase().replace(/[\W_]/g, '-')
const application = await createTeamApplication(request.session.User, team)
defaultTeamCreated = true
const instanceProperties = {
name: `${safeTeamName}-${safeUserName}-${crypto.randomBytes(4).toString('hex')}`
}
await app.db.controllers.Project.create(team, application, request.session.User, instanceType, instanceStack, instanceTemplate, instanceProperties)
}
}
} else {
const teamBillingDisabled = await teamType.getProperty('billing.disabled', false)
if (!teamBillingDisabled) {
const session = await app.billing.createSubscriptionSession(team, request.session.User)
app.auditLog.Team.billing.session.created(request.session.User, null, team, session)
teamView.billingURL = session.url
}
}
}
// Haven't created an application yet, but settings say we should
if (!defaultTeamCreated && app.settings.get('user:team:auto-create:application')) {
await createTeamApplication(request.session.User, team)
}
await appendBillingDetails(teamView, team, request)
reply.send(teamView)
} catch (err) {
// prepare response
const resp = { code: 'unexpected_error', error: err.toString() }
// update resp.error is the error has an errors array
if (err.errors) {
resp.error = err.errors.map(err => err.message).join(',')
}
// audit log to platform & team
await app.auditLog.Platform.platform.team.created(request.session.User, resp, team)
await app.auditLog.Team.team.created(request.session.User, resp, team)
// destroy team if it was created
if (team !== undefined) {
// safe to destroy because it will have only 1 owner and no projects
await team.destroy()
await app.auditLog.Platform.platform.team.deleted(0, null, team)
}
reply.code(400).send(resp)
}
})
/**
* Delete a team
* /api/v1/teams/:teamId
*/
app.delete('/:teamId', {
preHandler: app.needsPermission('team:delete'),
schema: {
summary: 'Delete a team',
tags: ['Teams'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
$ref: 'APIStatus'
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
// At this point we know the requesting user has permission to do this.
// But we also need to ensure the team has no projects
// That is handled by the beforeDestroy hook on the Team model and the
// call to destroy the team will throw an error
try {
const instanceCount = await request.team.instanceCount()
if (instanceCount > 0) {
// need to delete Instances
const instances = await app.db.models.Project.byTeam(request.team.hashid)
for (const instance of instances) {
try {
await app.containers.remove(instance)
} catch (err) {
if (err?.statusCode !== 404) {
throw err
}
}
await instance.destroy()
await app.auditLog.Team.project.deleted(request.session.User, null, request.team, instance)
await app.auditLog.Project.project.deleted(request.session.User, null, request.team, instance)
}
}
// Delete Applications
const applications = await app.db.models.Application.byTeam(request.team.hashid)
for (const application of applications) {
await application.destroy()
await app.auditLog.Team.application.deleted(request.session.User, null, request.team, application)
}
// Delete Devices
const where = {
TeamId: request.team.id
}
const devices = await app.db.models.Device.getAll({}, where, { includeInstanceApplication: true })
for (const device of devices.devices) {
await device.destroy()
await app.auditLog.Team.team.device.deleted(request.session.User, null, request.team, device)
}
await request.team.destroy()
await app.auditLog.Platform.platform.team.deleted(request.session.User, null, request.team)
await app.auditLog.Team.team.deleted(request.session.User, null, request.team)
reply.send({ status: 'okay' })
} catch (err) {
const resp = { code: 'unexpected_error', error: err.toString() }
await app.auditLog.Platform.platform.team.deleted(request.session.User, resp, request.team)
await app.auditLog.Team.team.deleted(request.session.User, resp, request.team)
reply.code(400).send(resp)
}
})
/**
* Update a team
* /api/v1/teams/:teamId
*/
app.put('/:teamId', {
preHandler: app.needsPermission('team:edit'),
schema: {
summary: 'Update a team',
tags: ['Teams'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
body: {
type: 'object',
properties: {
name: { type: 'string' },
slug: { type: 'string' },
type: { type: 'string' },
suspended: { type: 'boolean' }
}
},
response: {
200: {
$ref: 'Team'
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
let updates
let auditLogFunc = app.auditLog.Team.team.settings.updated
try {
if (request.body.type) {
auditLogFunc = app.auditLog.Team.team.type.changed
if (Object.keys(request.body).length > 1) {
reply.code(400).send({ code: 'invalid_request', error: 'Cannot modify other properties whilst changing type' })
return
}
if (request.body.type !== request.team.TeamType.hashid) {
const targetTeamType = await app.db.models.TeamType.byId(request.body.type)
if (!targetTeamType) {
reply.code(400).send({ code: 'invalid_team_type', error: 'Invalid team type' })
return
}
updates = {
old: { id: request.team.TeamType.hashid, name: request.team.TeamType.name },
new: { id: targetTeamType.hashid, name: targetTeamType.name }
}
// Two stage process to update team type
// - first we check its allowed.
await request.team.checkTeamTypeUpdateAllowed(targetTeamType)
// - then we apply it
await request.team.updateTeamType(targetTeamType)
} else {
reply.send(app.db.views.Team.team(request.team))
return
}
} else if (Object.hasOwn(request.body, 'suspended')) {
if (Object.keys(request.body).length > 1) {
reply.code(400).send({ code: 'invalid_request', error: 'Cannot modify other properties whilst changing suspended state' })
return
}
if (!!request.body.suspended !== !!request.team.suspended) {
let teamAuditFunc = app.auditLog.Team.team.suspended
let platformAuditFunc = app.auditLog.Platform.platform.team.suspended
try {
if (request.body.suspended) {
// Suspend the team
await request.team.suspend()
} else {
teamAuditFunc = app.auditLog.Team.team.unsuspended
platformAuditFunc = app.auditLog.Platform.platform.team.unsuspended
// Reactivate the team
await request.team.unsuspend()
}
teamAuditFunc(request.session.User, null, request.team)
platformAuditFunc(request.session.User, null, request.team)
reply.send(app.db.views.Team.team(request.team))
return
} catch (err) {
teamAuditFunc(request.session.User, err, request.team)
platformAuditFunc(request.session.User, err, request.team)
const response = {
code: err.code || 'unexpected_error',
error: err.toString()
}
reply.code(400).send(response)
return
}
} else {
// Already in the right state - no-op it
reply.send(app.db.views.Team.team(request.team))
return
}
} else {
updates = new app.auditLog.formatters.UpdatesCollection()
if (request.body.name) {
updates.push('name', request.team.name, request.body.name)
request.team.name = request.body.name
}
if (request.body.slug) {
if (request.body.slug === 'create') {
reply.code(400).send({ code: 'invalid_slug', error: 'slug not available' })
return
}
updates.push('slug', request.team.slug, request.body.slug)
request.team.slug = request.body.slug
}
await request.team.save()
}
auditLogFunc(request.session.User, null, request.team, updates)
reply.send(app.db.views.Team.team(request.team))
} catch (err) {
auditLogFunc(request.session.User, err, request.team, updates)
const response = {
code: err.code || 'unexpected_error',
error: err.toString()
}
if (/SequelizeUniqueConstraintError/.test(response.error)) {
if (err.errors) {
// This is an error from sequelize - reformat the message to be more helpful
response.error = err.errors.map(e => e.message).join(', ')
}
} else if (err.errors) {
response.errors = err.errors
}
reply.code(400).send(response)
}
})
/**
* Get the session users team membership
* @name /api/v1/team/:teamId/user
* @static
* @memberof forge.routes.api.team
*/
app.get('/:teamId/user', {
schema: {
summary: 'Get the current users team membership',
tags: ['Teams'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
role: { type: 'number' }
}
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
if (request.teamMembership) {
reply.send({
role: request.teamMembership.role
})
return
} else if (request.session.User?.admin) {
reply.send({
role: Roles.Admin
})
return
}
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
})
/**
* Get the team audit log
* @name /api/v1/team/:teamId/audit-log
* @memberof forge.routes.api.project
*/
app.get('/:teamId/audit-log', {
preHandler: app.needsPermission('team:audit-log'),
schema: {
summary: 'Get team audit event entries',
tags: ['Teams'],
query: {
allOf: [
{ $ref: 'PaginationParams' },
{ $ref: 'AuditLogQueryParams' }
]
},
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
meta: { $ref: 'PaginationMeta' },
count: { type: 'number' },
log: { $ref: 'AuditLogEntryList' },
associations: {
type: 'object',
properties: {
applications: {
type: 'array',
items: { $ref: 'ApplicationSummary' }
},
instances: { $ref: 'InstanceSummaryList' },
devices: { $ref: 'DeviceSummaryList' }
}
}
}
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
const paginationOptions = app.getPaginationOptions(request)
const logEntries = await app.db.models.AuditLog.forTeam(request.team.id, paginationOptions)
const result = app.db.views.AuditLog.auditLog(logEntries)
reply.send(result)
})
/**
* Get the team audit log
* @name /api/v1/team/:teamId/audit-log/export
* @memberof forge.routes.api.project
*/
app.get('/:teamId/audit-log/export', {
preHandler: app.needsPermission('team:audit-log'),
schema: {
summary: 'Get team audit event entries',
tags: ['Teams'],
query: {
allOf: [
{ $ref: 'PaginationParams' },
{ $ref: 'AuditLogQueryParams' }
]
},
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
content: {
'text/csv': {
schema: {
type: 'string'
}
}
}
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
const paginationOptions = app.getPaginationOptions(request)
const logEntries = await app.db.models.AuditLog.forTeam(request.team.id, paginationOptions)
const result = app.db.views.AuditLog.auditLog(logEntries)
reply.type('text/csv').send([
['id', 'event', 'body', 'scope', 'trigger', 'createdAt'],
...result.log.map(row => [
row.id,
row.event,
`"${row.body ? JSON.stringify(row.body).replace(/"/g, '""') : ''}"`,
`"${JSON.stringify(row.scope).replace(/"/g, '""')}"`,
`"${JSON.stringify(row.trigger).replace(/"/g, '""')}"`,
row.createdAt?.toISOString()
])
]
.map(row => row.join(','))
.join('\r\n'))
})
}