From 5536bc0f93986dd72d5164ac52155a4aa7596130 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Mon, 21 Jun 2021 15:17:31 +0300 Subject: [PATCH] added new API endpoint to get info about deleted users --- config/lmtp.toml | 2 +- docs/api/openapi.yml | 67 ++++++++++++++++++++++++++++++++++++++++++++ lib/acme/certs.js | 1 - lib/api/users.js | 60 +++++++++++++++++++++++++++++++++++++++ lib/user-handler.js | 34 ++++++++++++++++++++++ 5 files changed, 162 insertions(+), 2 deletions(-) diff --git a/config/lmtp.toml b/config/lmtp.toml index dff1814..3d9a9cd 100644 --- a/config/lmtp.toml +++ b/config/lmtp.toml @@ -1,6 +1,6 @@ # If enabled then WildDuck exposes a LMTP interface for pushing messages to mail store # NB! If you are using WildDuck plugin for Haraka then LMTP is not needed -enabled=true +enabled=false port=2424 # by default bind to localhost only diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index 93a4957..5a24401 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -2350,6 +2350,29 @@ paths: type: string '/users/{id}/restore': + get: + tags: + - Users + summary: Return recovery info for a deleted user + operationId: restoreUserInfo + parameters: + - name: sess + in: query + description: Session identifier for the logs + schema: + type: string + - name: ip + in: query + description: IP address for the logs + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/RecoverInfoResponse' post: tags: - Users @@ -4109,6 +4132,50 @@ components: description: Unique ID (24 byte hex) example: '609d201236d1d936948f23b1' + RecoverInfoResponse: + type: object + required: + - success + - user + - username + - storageUsed + - tags + - deleted + - recoverableAddresses + properties: + success: + type: boolean + description: Indicates successful response + example: true + user: + type: string + description: ID of the deleted User + example: '609d201236d1d936948f23b1' + username: + type: string + description: Username of the User + example: andris + storageUsed: + type: number + description: Calculated quota usage for the user + example: 2423070 + tags: + type: array + items: + type: string + description: List of tags associated with the User + example: ['domain:andrisreinman.com'] + deleted: + type: string + description: Datestring of the time the user was deleted + format: date-time + recoverableAddresses: + type: array + items: + type: string + description: List of email addresses that can be restored + example: ['andris@andrisreinman.com'] + GetAllowedDomainResponse: required: - success diff --git a/lib/acme/certs.js b/lib/acme/certs.js index 9f5c7ef..efa7a68 100644 --- a/lib/acme/certs.js +++ b/lib/acme/certs.js @@ -192,7 +192,6 @@ const acquireCert = async (domain, acmeOptions, certificateData, certHandler) => } let lock = await getLock(domainOpLockKey, 10 * 60 * 1000, 3 * 60 * 1000); - console.log(lock); if (!lock.success) { return certificateData; } diff --git a/lib/api/users.js b/lib/api/users.js index 60ccf27..1fe72d0 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -1401,6 +1401,66 @@ module.exports = (db, server, userHandler) => { }) ); + server.get( + '/users/:user/restore', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); + + const schema = Joi.object().keys({ + user: Joi.string().hex().lowercase().length(24).required(), + sess: sessSchema, + ip: sessIPSchema + }); + + const result = schema.validate(req.params, { + abortEarly: false, + convert: true + }); + + if (result.error) { + res.status(400); + res.json({ + error: result.error.message, + code: 'InputValidationError', + details: tools.validationErrors(result) + }); + return next(); + } + + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).readOwn('users')); + } else { + req.validate(roles.can(req.role).readAny('users')); + } + + let user = new ObjectID(result.value.user); + + let userInfo; + try { + userInfo = await userHandler.restoreInfo(user); + } catch (err) { + res.status(err.responseCode || 500); // TODO: use response code specific status + res.json({ + error: err.message, + code: err.code + }); + return next(); + } + + res.json( + Object.assign( + { + success: !!userInfo + }, + userInfo + ) + ); + + return next(); + }) + ); + server.post( '/users/:user/restore', tools.asyncifyJson(async (req, res, next) => { diff --git a/lib/user-handler.js b/lib/user-handler.js index 8aac524..9f2a3dd 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -3551,6 +3551,40 @@ class UserHandler { return result; } + // Return information about deleted user + async restoreInfo(user) { + let result = { + user: user.toString() + }; + + let existingAccount = await this.users.collection('deletedusers').findOne({ _id: user }); + if (!existingAccount) { + let err = new Error('Delete account was not found'); + err.responseCode = 404; + err.code = 'AccountNotFound'; + throw err; + } + + result.username = existingAccount.username; + result.storageUsed = existingAccount.storageUsed; + result.tags = existingAccount.tags; + + result.deleted = existingAccount.deleteInfo.deletedAt.toISOString(); + + // Step 2. restore addresses + let recoverableAddresses = []; + for (let address of existingAccount.deleteInfo.addresses || []) { + let existingAddress = await this.users.collection('addresses').findOne(address); + if (!existingAddress || existingAddress.user.equals(user)) { + recoverableAddresses.push(address.address); + } + } + + result.recoverableAddresses = recoverableAddresses; + + return result; + } + // This method restores a user that is queued for deletion async restore(user, options) { options = options || {};