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/sites.js

302 lines
7.5 KiB

import { validate as uuidValidate } from 'uuid'
import { CustomError } from '../helpers/common.js'
/**
* Sites API Routes
*/
async function routes(app) {
app.get(
'/',
{
config: {
permissions: ['read:sites', 'read:dashboard']
},
schema: {
summary: 'List all sites',
tags: ['Sites'],
response: {
200: {
description: 'List of all sites',
type: 'array',
items: { $ref: 'Site#' }
}
}
}
},
async () => {
const sites = await WIKI.models.sites.getAllSites()
return sites.map((s) => ({
...s.config,
id: s.id,
hostname: s.hostname,
isEnabled: s.isEnabled,
editors: {
asciidoc: s.config.editors?.asciidoc?.isActive ?? false,
markdown: s.config.editors?.markdown?.isActive ?? false,
wysiwyg: s.config.editors?.wysiwyg?.isActive ?? false
}
}))
}
)
app.get(
'/:siteIdorHostname',
{
schema: {
summary: 'Get site info',
tags: ['Sites'],
params: {
type: 'object',
properties: {
siteIdorHostname: {
type: 'string',
description: 'Either a site ID, hostname or "current" to use the request hostname.',
anyOf: [{ format: 'uuid' }, { enum: ['current'] }, { pattern: '^[a-z0-9.-]+$' }]
}
},
required: ['siteIdorHostname']
},
querystring: {
type: 'object',
properties: {
strict: {
type: 'boolean',
description:
'Whether to only return a site that exactly matches the hostname. Wildcard sites will not be matched.',
default: false
}
}
},
response: {
200: {
description: 'Site info',
type: 'object',
$ref: 'Site#'
}
}
}
},
async (req, reply) => {
let site
if (req.params.siteIdorHostname === 'current' && req.hostname) {
site = await WIKI.models.sites.getSiteByHostname({
hostname: req.hostname,
strict: req.querystring?.strict ?? false
})
} else if (uuidValidate(req.params.siteIdorHostname)) {
site = await WIKI.models.sites.getSiteById({ id: req.params.siteIdorHostname })
} else {
site = await WIKI.models.sites.getSiteByHostname({
hostname: req.params.siteIdorHostname,
strict: req.querystring?.strict ?? false
})
}
if (site) {
return {
...site.config,
id: site.id,
hostname: site.hostname,
isEnabled: site.isEnabled,
editors: {
asciidoc: site.config.editors?.asciidoc?.isActive ?? false,
markdown: site.config.editors?.markdown?.isActive ?? false,
wysiwyg: site.config.editors?.wysiwyg?.isActive ?? false
}
}
} else {
return reply.notFound('Site does not exist.')
}
}
)
/**
* CREATE SITE
*/
app.post(
'/',
{
config: {
permissions: ['create:sites', 'manage:sites']
},
schema: {
summary: 'Create a new site',
tags: ['Sites'],
body: {
type: 'object',
required: ['hostname', 'title'],
properties: {
hostname: {
type: 'string',
minLength: 1,
maxLength: 255,
pattern: '^(\\*|[a-z0-9.-]+)$'
},
title: {
type: 'string',
minLength: 1,
maxLength: 255
}
},
examples: [
{
hostname: 'wiki.example.org',
title: 'My Wiki Site'
}
]
},
response: {
200: {
description: 'Site created successfully',
type: 'object',
properties: {
ok: {
type: 'boolean'
},
message: {
type: 'string'
},
id: {
type: 'string',
format: 'uuid'
}
}
}
}
}
},
async (req, reply) => {
// -> Validate inputs
if (
!req.body.hostname ||
req.body.hostname.length < 1 ||
!/^(\\*)|([a-z0-9\-.:]+)$/.test(req.body.hostname)
) {
throw new CustomError('siteCreateInvalidHostname', 'Invalid Site Hostname')
}
if (!req.body.title || req.body.title.length < 1 || !/^[^<>"]+$/.test(req.body.title)) {
throw new CustomError('siteCreateInvalidTitle', 'Invalid Site Title')
}
// -> Check for duplicate hostname
if (!(await WIKI.models.sites.isHostnameUnique(req.body.hostname))) {
if (req.body.hostname === '*') {
throw new CustomError(
'siteCreateDuplicateCatchAll',
'A site with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.'
)
} else {
throw new CustomError(
'siteCreateDuplicateHostname',
'A site with a this hostname already exists! Cannot have duplicate hostnames.'
)
}
}
// -> Create site
try {
const result = await WIKI.models.sites.createSite(req.body.hostname, {
title: req.body.title
})
return {
ok: true,
message: 'Site created successfully.',
id: result.id
}
} catch (err) {
WIKI.logger.warn(err)
return reply.internalServerError()
}
}
)
/**
* UPDATE SITE
*/
app.put(
'/:siteId',
{
config: {
permissions: ['manage:sites']
},
schema: {
summary: 'Update a site',
tags: ['Sites'],
body: {
type: 'object',
properties: {
isEnabled: {
type: 'boolean'
},
hostname: {
type: 'string',
minLength: 1,
maxLength: 255,
pattern: '^(\\*|[a-z0-9.-]+)$'
},
title: {
type: 'string',
minLength: 1,
maxLength: 255
}
},
examples: [
{
hostname: 'wiki.example.org',
title: 'My Wiki Site'
}
]
}
}
},
async () => {
return { hello: 'world' }
}
)
/**
* DELETE SITE
*/
app.delete(
'/:siteId',
{
config: {
permissions: ['manage:sites']
},
schema: {
summary: 'Delete a site',
tags: ['Sites'],
params: {
type: 'object',
properties: {
siteId: {
type: 'string',
format: 'uuid'
}
},
required: ['siteId']
},
response: {
204: {
description: 'Site deleted successfully'
}
}
}
},
async (req, reply) => {
try {
if ((await WIKI.models.sites.countSites()) <= 1) {
reply.conflict('Cannot delete the last site. At least 1 site must exist at all times.')
} else if (await WIKI.models.sites.deleteSite(req.params.siteId)) {
reply.code(204)
} else {
reply.badRequest('Site does not exist.')
}
} catch (err) {
reply.send(err)
}
}
)
}
export default routes