From 95f829d16aa24883bd763179581b5288acc51f3d Mon Sep 17 00:00:00 2001 From: NickOvt Date: Mon, 5 Feb 2024 10:30:02 +0200 Subject: [PATCH] fix(api-filters): Add Filters API endpoints to API docs generation ZMS-121 (#611) * added List all FIltes endpoint to api docs generation * List Filters for a User endpoint added to API docs generation * Request Filter information endpoint added to API docs generation. Add schemas * fix description * added Delete a Filter api endpoint to API docs generation * New schemas for filter api endpoints. Added Create a mew Filter api endpoint to API docs generation * fix schemas. Added Update Filter information endpoint to api docs generation --- lib/api/filters.js | 332 ++++++++++++++++-------- lib/schemas/request/filters-schemas.js | 47 ++++ lib/schemas/request/general-schemas.js | 4 +- lib/schemas/response/filters-schemas.js | 28 ++ 4 files changed, 299 insertions(+), 112 deletions(-) create mode 100644 lib/schemas/request/filters-schemas.js create mode 100644 lib/schemas/response/filters-schemas.js diff --git a/lib/api/filters.js b/lib/api/filters.js index 0ed4e090..47ee7364 100644 --- a/lib/api/filters.js +++ b/lib/api/filters.js @@ -9,22 +9,55 @@ const tools = require('../tools'); const roles = require('../roles'); const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas'); const { publish, FILTER_DELETED, FILTER_CREATED, FORWARD_ADDED } = require('../events'); +const { successRes, totalRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas'); +const { GetAllFiltersResult, GetFiltersResult } = require('../schemas/response/filters-schemas'); +const { FilterQuery, FilterAction } = require('../schemas/request/filters-schemas'); +const { userId, filterId } = require('../schemas/request/general-schemas'); module.exports = (db, server, userHandler, settingsHandler) => { server.get( - { name: 'filters', path: '/filters' }, + { + name: 'filters', + path: '/filters', + summary: 'List all Filters', + tags: ['Filters'], + validationObjs: { + requestBody: {}, + queryParams: { + forward: Joi.string().trim().empty('').max(255).description('Partial match of a forward email address or URL'), + metaData: booleanSchema.description('If true, then includes metaData in the response'), + 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: Joi.number().required().description('Current page number. Derived from page query argument.'), + previousCursor: previousCursorRes, + nextCursor: nextCursorRes, + results: Joi.array().items(GetAllFiltersResult).required().description('Address listing') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - forward: Joi.string().trim().empty('').max(255), - metaData: booleanSchema, - limit: Joi.number().default(20).min(1).max(250), - next: nextPageCursorSchema, - previous: previousPageCursorSchema, - page: pageNrSchema, - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...requestBody, + ...queryParams }); const result = schema.validate(req.params, { @@ -173,15 +206,46 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.get( - '/users/:user/filters', + { + path: '/users/:user/filters', + summary: 'List Filters for a User', + tags: ['Filters'], + validationObjs: { + requestBody: {}, + queryParams: { + metaData: booleanSchema.description('If true, then includes metaData in the response'), + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { + user: userId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + limits: Joi.object({ + allowed: Joi.number().description('How many filters are allowed'), + used: Joi.number().description('How many filters have been created') + }) + .required() + .description('Filter usage limits for the user account'), + results: Joi.array().items(GetFiltersResult).required().description('Filter description') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - metaData: booleanSchema, - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...requestBody, + ...queryParams }); const result = schema.validate(req.params, { @@ -317,15 +381,46 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.get( - '/users/:user/filters/:filter', + { + path: '/users/:user/filters/:filter', + summary: 'Request Filter information', + tags: ['Filters'], + validationObjs: { + requestBody: {}, + queryParams: { + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { + user: userId, + filter: filterId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + id: filterId, + name: Joi.string().required().description('Name for the filter'), + created: Joi.date().required().description('Datestring of the time the filter was created'), + query: FilterQuery.required(), + action: FilterAction.required(), + disabled: booleanSchema.required().description('If true, then this filter is ignored'), + metaData: Joi.object().description('Custom metadata value') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - filter: Joi.string().hex().lowercase().length(24).required(), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...requestBody, + ...queryParams }); const result = schema.validate(req.params, { @@ -443,15 +538,39 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.del( - '/users/:user/filters/:filter', + { + path: '/users/:user/filters/:filter', + summary: 'Delete a Filter', + tags: ['Filters'], + validationObjs: { + requestBody: {}, + queryParams: { + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { + user: userId, + filter: filterId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - filter: Joi.string().hex().lowercase().length(24).required(), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...requestBody, + ...queryParams }); const result = schema.validate(req.params, { @@ -514,52 +633,48 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.post( - '/users/:user/filters', - tools.responseWrapper(async (req, res) => { - res.charSet('utf-8'); + { + path: '/users/:user/filters', + summary: 'Create a new Filter', + tags: ['Filters'], + validationObjs: { + requestBody: { + name: Joi.string().trim().max(255).empty('').description('Name of the Filter'), - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), + query: FilterQuery, + action: FilterAction, - name: Joi.string().trim().max(255).empty(''), + disabled: booleanSchema.default(false).description('If true then this filter is ignored'), - query: Joi.object() - .keys({ - from: Joi.string().trim().max(255).empty(''), - to: Joi.string().trim().max(255).empty(''), - subject: Joi.string().trim().max(255).empty(''), - listId: Joi.string().trim().max(255).empty(''), - text: Joi.string().trim().max(255).empty(''), - ha: booleanSchema, - size: Joi.number().empty('') - }) - .default({}), - action: Joi.object() - .keys({ - seen: booleanSchema, - flag: booleanSchema, - delete: booleanSchema, - spam: booleanSchema, - mailbox: Joi.string().hex().lowercase().length(24).empty(''), - targets: Joi.array() - .items( - Joi.string().email({ tlds: false }), - Joi.string().uri({ - scheme: [/smtps?/, /https?/], - allowRelative: false, - relativeOnly: false - }) - ) - .empty('') - }) - .default({}), + metaData: metaDataSchema.label('metaData').description('Optional metadata, must be an object or JSON formatted string'), - disabled: booleanSchema.default(false), + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: { + user: userId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + id: Joi.string().required().description('ID for the created filter') + }) + } + } + } + }, + tools.responseWrapper(async (req, res) => { + res.charSet('utf-8'); - metaData: metaDataSchema.label('metaData'), + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; - sess: sessSchema, - ip: sessIPSchema + const schema = Joi.object({ + ...pathParams, + ...requestBody, + ...queryParams }); const result = schema.validate(req.params, { @@ -788,53 +903,48 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.put( - '/users/:user/filters/:filter', - tools.responseWrapper(async (req, res) => { - res.charSet('utf-8'); + { + path: '/users/:user/filters/:filter', + summary: 'Update Filter information', + tags: ['Filters'], + validationObjs: { + requestBody: { + name: Joi.string().trim().max(255).empty('').description('Name of the Filter'), - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - filter: Joi.string().hex().lowercase().length(24).required(), - - name: Joi.string().trim().max(255).empty(''), - - query: Joi.object() - .keys({ - from: Joi.string().trim().max(255).empty(''), - to: Joi.string().trim().max(255).empty(''), - subject: Joi.string().trim().max(255).empty(''), - listId: Joi.string().trim().max(255).empty(''), - text: Joi.string().trim().max(255).empty(''), - ha: booleanSchema, - size: Joi.number().empty('') - }) - .default({}), - action: Joi.object() - .keys({ - seen: booleanSchema, - flag: booleanSchema, - delete: booleanSchema, - spam: booleanSchema, - mailbox: Joi.string().hex().lowercase().length(24).empty(''), - targets: Joi.array() - .items( - Joi.string().email({ tlds: false }), - Joi.string().uri({ - scheme: [/smtps?/, /https?/], - allowRelative: false, - relativeOnly: false - }) - ) - .empty('') - }) - .default({}), + query: FilterQuery, + action: FilterAction, + + disabled: booleanSchema.description('If true then this filter is ignored'), - disabled: booleanSchema, + metaData: metaDataSchema.label('metaData').description('Optional metadata, must be an object or JSON formatted string'), + + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: { + user: userId, + filter: filterId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes + }) + } + } + } + }, + tools.responseWrapper(async (req, res) => { + res.charSet('utf-8'); - metaData: metaDataSchema.label('metaData'), + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; - sess: sessSchema, - ip: sessIPSchema + const schema = Joi.object({ + ...pathParams, + ...requestBody, + ...queryParams }); const result = schema.validate(req.params, { diff --git a/lib/schemas/request/filters-schemas.js b/lib/schemas/request/filters-schemas.js new file mode 100644 index 00000000..115d9223 --- /dev/null +++ b/lib/schemas/request/filters-schemas.js @@ -0,0 +1,47 @@ +'use strict'; + +const Joi = require('joi'); +const { booleanSchema } = require('../../schemas'); + +const FilterAction = Joi.object({ + seen: booleanSchema.description('If true then mark matching messages as Seen'), + flag: booleanSchema.description('If true then mark matching messages as Flagged'), + delete: booleanSchema.description('If true then do not store matching messages'), + spam: booleanSchema.description('If true then store matching messags to Junk Mail folder'), + mailbox: Joi.string().hex().lowercase().length(24).empty('').description('Mailbox ID to store matching messages to'), + targets: Joi.array() + .items( + Joi.string().email({ tlds: false }), + Joi.string().uri({ + scheme: [/smtps?/, /https?/], + allowRelative: false, + relativeOnly: false + }) + ) + .empty('') + .description( + 'An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to' + ) +}) + .default({}) + .description('Action to take with a matching message') + .$_setFlag('objectName', 'Action'); + +const FilterQuery = Joi.object({ + from: Joi.string().trim().max(255).empty('').description('Partial match for the From: header (case insensitive)'), + to: Joi.string().trim().max(255).empty('').description('Partial match for the To:/Cc: headers (case insensitive)'), + subject: Joi.string().trim().max(255).empty('').description('Partial match for the Subject: header (case insensitive)'), + listId: Joi.string().trim().max(255).empty('').description('Partial match for the List-ID: header (case insensitive)'), + text: Joi.string().trim().max(255).empty('').description('Fulltext search against message text'), + ha: booleanSchema.description('Does a message have to have an attachment or not'), + size: Joi.number() + .empty('') + .description( + 'Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value' + ) +}) + .default({}) + .description('Rules that a message must match') + .$_setFlag('objectName', 'Query'); + +module.exports = { FilterAction, FilterQuery }; diff --git a/lib/schemas/request/general-schemas.js b/lib/schemas/request/general-schemas.js index 398b9c72..ca348be2 100644 --- a/lib/schemas/request/general-schemas.js +++ b/lib/schemas/request/general-schemas.js @@ -7,11 +7,13 @@ const mailboxId = Joi.string().hex().lowercase().length(24).required().descripti const messageId = Joi.number().min(1).required().description('Message ID'); const addressEmail = Joi.string().email({ tlds: false }).required().description('E-mail Address'); const addressId = Joi.string().hex().lowercase().length(24).required().description('ID of the Address'); +const filterId = Joi.string().hex().lowercase().length(24).required().description('Filters unique ID'); module.exports = { userId, mailboxId, messageId, addressEmail, - addressId + addressId, + filterId }; diff --git a/lib/schemas/response/filters-schemas.js b/lib/schemas/response/filters-schemas.js new file mode 100644 index 00000000..dd2e7921 --- /dev/null +++ b/lib/schemas/response/filters-schemas.js @@ -0,0 +1,28 @@ +'use strict'; + +const Joi = require('joi'); +const { booleanSchema } = require('../../schemas'); + +const GetAllFiltersResult = Joi.object({ + id: Joi.string().required().description('Filter ID'), + user: Joi.string().required().description('User ID'), + name: Joi.string().required().description('Name for the filter'), + created: Joi.date().required().description('Datestring of the time the filter was created'), + query: Joi.array().items(Joi.array().items(Joi.string())).required().description('Filter query strings'), + action: Joi.array().items(Joi.array().items(Joi.string())).required().description('Filter action strings'), + disabled: booleanSchema.required().description('If true, then this filter is ignored'), + metaData: Joi.object().description('Custom metadata value. Included if metaData query argument was true'), + targets: Joi.array().items(Joi.string()).description('List of forwarding targets') +}).$_setFlag('objectName', 'GetAllFiltersResult'); + +const GetFiltersResult = Joi.object({ + id: Joi.string().required().description('Filter ID'), + name: Joi.string().required().description('Name for the filter'), + created: Joi.date().required().description('Datestring of the time the filter was created'), + query: Joi.array().items(Joi.array().items(Joi.string())).required().description('Filter query strings'), + action: Joi.array().items(Joi.array().items(Joi.string())).required().description('Filter action strings'), + disabled: booleanSchema.required().description('If true, then this filter is ignored'), + metaData: Joi.object().description('Custom metadata value. Included if metaData query argument was true') +}).$_setFlag('objectName', 'GetFiltersResult'); + +module.exports = { GetAllFiltersResult, GetFiltersResult };