2020-10-09 16:08:33 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const Joi = require('joi');
|
|
|
|
const MongoPaging = require('mongo-cursor-pagination');
|
2021-08-30 16:18:42 +08:00
|
|
|
const ObjectId = require('mongodb').ObjectId;
|
2020-10-09 16:08:33 +08:00
|
|
|
const tools = require('../tools');
|
|
|
|
const roles = require('../roles');
|
|
|
|
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema } = require('../schemas');
|
2024-05-09 16:12:28 +08:00
|
|
|
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
|
2020-10-09 16:08:33 +08:00
|
|
|
|
|
|
|
module.exports = (db, server) => {
|
|
|
|
server.get(
|
2024-05-09 16:12:28 +08:00
|
|
|
{
|
|
|
|
name: 'webhooks',
|
|
|
|
path: '/webhooks',
|
|
|
|
tags: ['Webhooks'],
|
|
|
|
summary: 'List registered Webhooks',
|
|
|
|
validationObjs: {
|
|
|
|
requestBody: {},
|
|
|
|
queryParams: {
|
|
|
|
type: Joi.string()
|
|
|
|
.empty('')
|
|
|
|
.lowercase()
|
|
|
|
.max(128)
|
|
|
|
.description('Prefix or exact match. Prefix match must end with ".*", eg "channel.*". Use "*" for all types'),
|
|
|
|
user: Joi.string().hex().lowercase().length(24).description('User ID'),
|
|
|
|
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
|
|
|
|
next: nextPageCursorSchema,
|
|
|
|
previous: previousPageCursorSchema,
|
|
|
|
page: pageNrSchema,
|
|
|
|
sess: sessSchema,
|
|
|
|
ip: sessIPSchema
|
|
|
|
},
|
|
|
|
pathParams: {},
|
|
|
|
response: {
|
|
|
|
200: {
|
|
|
|
description: 'Success',
|
|
|
|
model: Joi.object({
|
|
|
|
success: successRes,
|
|
|
|
total: totalRes,
|
|
|
|
page: pageRes,
|
|
|
|
previousCursor: previousCursorRes,
|
|
|
|
nextCursor: nextCursorRes,
|
|
|
|
results: Joi.array()
|
|
|
|
.items(
|
|
|
|
Joi.object({
|
|
|
|
id: Joi.string().required().description('Webhooks unique ID (24 byte hex)'),
|
|
|
|
type: Joi.array().items(Joi.string()).required().description('An array of event types this webhook matches'),
|
|
|
|
user: Joi.string().required().description('User ID or null'),
|
|
|
|
url: Joi.string().required().description('Webhook URL')
|
|
|
|
}).$_setFlag('objectName', 'GetWebhooksResult')
|
|
|
|
)
|
|
|
|
.required()
|
|
|
|
.description('Webhook listing')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2022-11-24 18:26:10 +08:00
|
|
|
tools.responseWrapper(async (req, res) => {
|
2020-10-09 16:08:33 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
2024-05-09 16:12:28 +08:00
|
|
|
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;
|
|
|
|
|
|
|
|
const schema = Joi.object({
|
|
|
|
...pathParams,
|
|
|
|
...requestBody,
|
|
|
|
...queryParams
|
2020-10-09 16:08:33 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const result = schema.validate(req.params, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true,
|
|
|
|
allowUnknown: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.status(400);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError',
|
|
|
|
details: tools.validationErrors(result)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let permission;
|
|
|
|
let ownOnly = false;
|
|
|
|
permission = roles.can(req.role).readAny('webhooks');
|
2021-08-30 16:18:42 +08:00
|
|
|
if (!permission.granted && req.user && ObjectId.isValid(req.user)) {
|
2020-10-09 16:08:33 +08:00
|
|
|
permission = roles.can(req.role).readOwn('webhooks');
|
|
|
|
if (permission.granted) {
|
|
|
|
ownOnly = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// permissions check
|
|
|
|
req.validate(permission);
|
|
|
|
|
|
|
|
let query = {};
|
|
|
|
|
|
|
|
if (result.value.type) {
|
|
|
|
query.type = result.value.type;
|
|
|
|
}
|
|
|
|
|
2021-08-30 16:18:42 +08:00
|
|
|
let user = result.value.user ? new ObjectId(result.value.user) : null;
|
2020-10-09 16:08:33 +08:00
|
|
|
if (ownOnly) {
|
2021-08-30 16:18:42 +08:00
|
|
|
user = new ObjectId(req.user);
|
2020-10-09 16:08:33 +08:00
|
|
|
}
|
|
|
|
if (user) {
|
|
|
|
query.user = user;
|
|
|
|
}
|
|
|
|
|
|
|
|
let limit = result.value.limit;
|
|
|
|
let page = result.value.page;
|
|
|
|
let pageNext = result.value.next;
|
|
|
|
let pagePrevious = result.value.previous;
|
|
|
|
|
|
|
|
let total = await db.users.collection('webhooks').countDocuments(query);
|
|
|
|
|
|
|
|
let opts = {
|
|
|
|
limit,
|
|
|
|
query,
|
|
|
|
fields: {
|
|
|
|
// FIXME: hack to keep _id in response
|
|
|
|
_id: true,
|
|
|
|
// FIXME: MongoPaging inserts fields value as second argument to col.find()
|
|
|
|
projection: {
|
|
|
|
_id: true,
|
|
|
|
type: true,
|
|
|
|
user: true,
|
|
|
|
url: true
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// _id gets removed in response if not explicitly set in paginatedField
|
|
|
|
paginatedField: '_id',
|
|
|
|
sortAscending: true
|
|
|
|
};
|
|
|
|
|
|
|
|
if (pageNext) {
|
|
|
|
opts.next = pageNext;
|
|
|
|
} else if ((!page || page > 1) && pagePrevious) {
|
|
|
|
opts.previous = pagePrevious;
|
|
|
|
}
|
|
|
|
|
|
|
|
let listing;
|
|
|
|
try {
|
|
|
|
listing = await MongoPaging.find(db.users.collection('webhooks'), opts);
|
|
|
|
} catch (err) {
|
2021-05-20 19:47:20 +08:00
|
|
|
res.status(500);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!listing.hasPrevious) {
|
|
|
|
page = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let response = {
|
|
|
|
success: true,
|
|
|
|
type: result.value.type,
|
|
|
|
user,
|
|
|
|
total,
|
|
|
|
page,
|
|
|
|
previousCursor: listing.hasPrevious ? listing.previous : false,
|
|
|
|
nextCursor: listing.hasNext ? listing.next : false,
|
|
|
|
results: (listing.results || []).map(webhookData => {
|
|
|
|
let values = {
|
|
|
|
id: webhookData._id.toString(),
|
|
|
|
type: webhookData.type,
|
|
|
|
user: webhookData.user ? webhookData.user.toString() : null,
|
|
|
|
url: webhookData.url
|
|
|
|
};
|
|
|
|
|
|
|
|
return permission.filter(values);
|
|
|
|
})
|
|
|
|
};
|
|
|
|
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json(response);
|
2020-10-09 16:08:33 +08:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
server.post(
|
2024-05-09 16:12:28 +08:00
|
|
|
{
|
|
|
|
path: '/webhooks',
|
|
|
|
tags: ['Webhooks'],
|
|
|
|
summary: 'Create new Webhook',
|
|
|
|
validationObjs: {
|
|
|
|
requestBody: {
|
|
|
|
type: Joi.array()
|
|
|
|
.items(Joi.string().trim().max(128).lowercase())
|
|
|
|
.required()
|
|
|
|
.description('An array of event types to match. For prefix match use ".*" at the end (eg. "user.*") or "*" for all types'),
|
|
|
|
user: Joi.string().hex().lowercase().length(24).description('User ID to match (only makes sense for user specific resources)'),
|
|
|
|
url: Joi.string()
|
|
|
|
.uri({ scheme: [/smtps?/, /https?/], allowRelative: false, relativeOnly: false })
|
|
|
|
.required()
|
|
|
|
.description('URL to POST data to'),
|
|
|
|
sess: sessSchema,
|
|
|
|
ip: sessIPSchema
|
|
|
|
},
|
|
|
|
queryParams: {},
|
|
|
|
pathParams: {},
|
|
|
|
response: {
|
|
|
|
200: {
|
|
|
|
description: 'Success',
|
|
|
|
model: Joi.object({
|
|
|
|
success: successRes,
|
|
|
|
id: Joi.string().required().description('ID of the Webhook')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2022-11-24 18:26:10 +08:00
|
|
|
tools.responseWrapper(async (req, res) => {
|
2020-10-09 16:08:33 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
2024-05-09 16:12:28 +08:00
|
|
|
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;
|
|
|
|
|
|
|
|
const schema = Joi.object({
|
|
|
|
...pathParams,
|
|
|
|
...requestBody,
|
|
|
|
...queryParams
|
2020-10-09 16:08:33 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const result = schema.validate(req.params, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.status(400);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError',
|
|
|
|
details: tools.validationErrors(result)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// permissions check
|
|
|
|
let permission;
|
|
|
|
if (req.user && req.user === result.value.user) {
|
|
|
|
permission = roles.can(req.role).createOwn('webhooks');
|
|
|
|
} else {
|
|
|
|
permission = roles.can(req.role).createAny('webhooks');
|
|
|
|
}
|
|
|
|
|
|
|
|
req.validate(permission);
|
|
|
|
|
|
|
|
result.value = permission.filter(result.value);
|
|
|
|
|
|
|
|
let type = result.value.type;
|
2021-08-30 16:18:42 +08:00
|
|
|
let user = result.value.user ? new ObjectId(result.value.user) : null;
|
2020-10-09 16:08:33 +08:00
|
|
|
let url = result.value.url;
|
|
|
|
|
|
|
|
let userData;
|
|
|
|
if (user) {
|
|
|
|
try {
|
|
|
|
userData = await db.users.collection('users').findOne(
|
|
|
|
{
|
|
|
|
_id: user
|
|
|
|
},
|
|
|
|
{
|
|
|
|
projection: {
|
|
|
|
address: true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
} catch (err) {
|
2021-05-20 19:47:20 +08:00
|
|
|
res.status(500);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!userData) {
|
2021-05-20 19:47:20 +08:00
|
|
|
res.status(404);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: 'This user does not exist',
|
|
|
|
code: 'UserNotFound'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let webhookData = {
|
|
|
|
type,
|
|
|
|
user,
|
|
|
|
url,
|
|
|
|
created: new Date()
|
|
|
|
};
|
|
|
|
|
|
|
|
let r;
|
|
|
|
// insert alias address to email address registry
|
|
|
|
try {
|
|
|
|
r = await db.users.collection('webhooks').insertOne(webhookData);
|
|
|
|
} catch (err) {
|
2021-05-20 19:47:20 +08:00
|
|
|
res.status(500);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let insertId = r.insertedId;
|
|
|
|
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
success: !!insertId,
|
|
|
|
id: insertId
|
|
|
|
});
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
server.del(
|
2024-05-09 16:12:28 +08:00
|
|
|
{
|
|
|
|
path: '/webhooks/:webhook',
|
|
|
|
tags: ['Webhooks'],
|
|
|
|
summary: 'Delete a webhook',
|
|
|
|
validationObjs: {
|
|
|
|
requestBody: {},
|
|
|
|
queryParams: {
|
|
|
|
sess: sessSchema,
|
|
|
|
ip: sessIPSchema
|
|
|
|
},
|
|
|
|
pathParams: { webhook: Joi.string().hex().lowercase().length(24).required().description('ID of the Webhook') },
|
|
|
|
response: {
|
|
|
|
200: {
|
|
|
|
description: 'Success',
|
|
|
|
model: Joi.object({ success: successRes })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2022-11-24 18:26:10 +08:00
|
|
|
tools.responseWrapper(async (req, res) => {
|
2020-10-09 16:08:33 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
2024-05-09 16:12:28 +08:00
|
|
|
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;
|
|
|
|
|
|
|
|
const schema = Joi.object({
|
|
|
|
...pathParams,
|
|
|
|
...requestBody,
|
|
|
|
...queryParams
|
2020-10-09 16:08:33 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const result = schema.validate(req.params, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.status(400);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError',
|
|
|
|
details: tools.validationErrors(result)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-30 16:18:42 +08:00
|
|
|
let webhook = new ObjectId(result.value.webhook);
|
2020-10-09 16:08:33 +08:00
|
|
|
|
|
|
|
let webhookData;
|
|
|
|
try {
|
|
|
|
webhookData = await db.users.collection('webhooks').findOne({
|
|
|
|
_id: webhook
|
|
|
|
});
|
|
|
|
} catch (err) {
|
2021-05-20 19:47:20 +08:00
|
|
|
res.status(500);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// permissions check
|
|
|
|
if (req.user && webhookData && webhookData.user && req.user === webhookData.user.toString()) {
|
|
|
|
req.validate(roles.can(req.role).deleteOwn('webhooks'));
|
|
|
|
} else {
|
|
|
|
req.validate(roles.can(req.role).deleteAny('webhooks'));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!webhookData) {
|
|
|
|
res.status(404);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: 'Invalid or unknown webhook identifier',
|
|
|
|
code: 'WebhookNotFound'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// delete address from email address registry
|
|
|
|
let r;
|
|
|
|
try {
|
|
|
|
r = await db.users.collection('webhooks').deleteOne({
|
|
|
|
_id: webhook
|
|
|
|
});
|
|
|
|
} catch (err) {
|
2021-05-20 19:47:20 +08:00
|
|
|
res.status(500);
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-24 18:26:10 +08:00
|
|
|
return res.json({
|
2020-10-09 16:08:33 +08:00
|
|
|
success: !!r.deletedCount
|
|
|
|
});
|
|
|
|
})
|
|
|
|
);
|
|
|
|
};
|