emailengine-app 2.68.1 → 2.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.github/workflows/deploy.yml +8 -3
  2. package/.github/workflows/release.yaml +6 -0
  3. package/CHANGELOG.md +59 -0
  4. package/Gruntfile.js +3 -1
  5. package/config/default.toml +2 -0
  6. package/data/google-crawlers.json +7 -1
  7. package/getswagger.sh +40 -4
  8. package/gettext-extract.js +163 -0
  9. package/lib/account.js +135 -72
  10. package/lib/api-routes/account-routes.js +684 -106
  11. package/lib/api-routes/blocklist-routes.js +344 -0
  12. package/lib/api-routes/chat-routes.js +32 -14
  13. package/lib/api-routes/delivery-test-routes.js +346 -0
  14. package/lib/api-routes/export-routes.js +28 -14
  15. package/lib/api-routes/gateway-routes.js +427 -0
  16. package/lib/api-routes/license-routes.js +156 -0
  17. package/lib/api-routes/mailbox-routes.js +344 -0
  18. package/lib/api-routes/message-routes.js +221 -187
  19. package/lib/api-routes/oauth2-app-routes.js +697 -0
  20. package/lib/api-routes/outbox-routes.js +185 -0
  21. package/lib/api-routes/pubsub-routes.js +102 -0
  22. package/lib/api-routes/route-helpers.js +58 -0
  23. package/lib/api-routes/settings-routes.js +357 -0
  24. package/lib/api-routes/stats-routes.js +111 -0
  25. package/lib/api-routes/submit-routes.js +461 -0
  26. package/lib/api-routes/template-routes.js +60 -75
  27. package/lib/api-routes/token-routes.js +297 -0
  28. package/lib/api-routes/webhook-route-routes.js +181 -0
  29. package/lib/autodetect-imap-settings.js +0 -2
  30. package/lib/consts.js +5 -0
  31. package/lib/email-client/base-client.js +28 -6
  32. package/lib/email-client/gmail-client.js +133 -112
  33. package/lib/email-client/imap/mailbox.js +34 -11
  34. package/lib/email-client/imap/subconnection.js +20 -13
  35. package/lib/email-client/imap/sync-operations.js +131 -3
  36. package/lib/email-client/imap-client.js +152 -75
  37. package/lib/email-client/notification-handler.js +1 -4
  38. package/lib/email-client/outlook-client.js +134 -75
  39. package/lib/export.js +97 -20
  40. package/lib/feature-flags.js +2 -2
  41. package/lib/gateway.js +4 -9
  42. package/lib/get-raw-email.js +5 -5
  43. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  44. package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
  45. package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
  46. package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
  47. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  48. package/lib/logger.js +24 -21
  49. package/lib/message-port-stream.js +113 -16
  50. package/lib/metrics-collector.js +0 -2
  51. package/lib/oauth2-apps.js +13 -4
  52. package/lib/outbox.js +24 -40
  53. package/lib/redis-operations.js +1 -1
  54. package/lib/reject-worker-calls.js +42 -0
  55. package/lib/routes-ui.js +37 -8778
  56. package/lib/schemas.js +429 -84
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +70 -4
  63. package/lib/ui-routes/account-routes.js +45 -212
  64. package/lib/ui-routes/admin-config-routes.js +928 -489
  65. package/lib/ui-routes/admin-entities-routes.js +1 -0
  66. package/lib/ui-routes/auth-routes.js +1339 -0
  67. package/lib/ui-routes/dashboard-routes.js +188 -0
  68. package/lib/ui-routes/document-store-routes.js +800 -0
  69. package/lib/ui-routes/export-routes.js +217 -0
  70. package/lib/ui-routes/internals-routes.js +354 -0
  71. package/lib/ui-routes/network-config-routes.js +759 -0
  72. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
  73. package/lib/ui-routes/route-helpers.js +314 -0
  74. package/lib/ui-routes/smtp-test-routes.js +236 -0
  75. package/lib/ui-routes/unsubscribe-routes.js +232 -0
  76. package/lib/webhook-request.js +36 -0
  77. package/lib/webhooks.js +8 -4
  78. package/package.json +13 -12
  79. package/sbom.json +1 -1
  80. package/server.js +222 -39
  81. package/static/licenses.html +160 -300
  82. package/translations/messages.pot +112 -132
  83. package/update-info.sh +19 -1
  84. package/views/config/logging.hbs +48 -0
  85. package/views/dashboard.hbs +7 -26
  86. package/views/internals/index.hbs +15 -0
  87. package/views/tokens/index.hbs +9 -0
  88. package/workers/api.js +200 -4424
  89. package/workers/documents.js +2 -22
  90. package/workers/export.js +103 -104
  91. package/workers/imap-proxy.js +3 -23
  92. package/workers/imap.js +32 -36
  93. package/workers/smtp.js +2 -22
  94. package/workers/submit.js +26 -35
  95. package/workers/webhooks.js +9 -43
@@ -4,11 +4,14 @@ const { redis } = require('../db');
4
4
  const { Account } = require('../account');
5
5
  const getSecret = require('../get-secret');
6
6
  const { templates } = require('../templates');
7
- const Boom = require('@hapi/boom');
8
7
  const Joi = require('joi');
9
8
  const { failAction } = require('../tools');
9
+ const { handleError, throwNotFound } = require('./route-helpers');
10
10
 
11
- const { templateSchemas, accountIdSchema } = require('../schemas');
11
+ const { templateSchemas, accountIdSchema, errorResponses } = require('../schemas');
12
+
13
+ // Template owner field shared by all template response schemas
14
+ const templateAccountIdSchema = accountIdSchema.required().allow(null).description('Account ID. Null for public templates');
12
15
 
13
16
  async function init(args) {
14
17
  const { server, call, CORS_CONFIG } = args;
@@ -43,15 +46,7 @@ async function init(args) {
43
46
  request.payload.content
44
47
  );
45
48
  } catch (err) {
46
- request.logger.error({ msg: 'API request failed', err });
47
- if (Boom.isBoom(err)) {
48
- throw err;
49
- }
50
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
51
- if (err.code) {
52
- error.output.payload.code = err.code;
53
- }
54
- throw error;
49
+ handleError(request, err);
55
50
  }
56
51
  },
57
52
 
@@ -60,7 +55,11 @@ async function init(args) {
60
55
  notes: 'Create a new stored template. Templates can be used when sending emails as the content of the message.',
61
56
  tags: ['api', 'Templates'],
62
57
 
63
- plugins: {},
58
+ plugins: {
59
+ 'hapi-swagger': {
60
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
61
+ }
62
+ },
64
63
 
65
64
  auth: {
66
65
  strategy: 'api-token',
@@ -112,7 +111,7 @@ async function init(args) {
112
111
  response: {
113
112
  schema: Joi.object({
114
113
  created: Joi.boolean().description('Was the template created or not'),
115
- account: accountIdSchema.required(),
114
+ account: templateAccountIdSchema,
116
115
  id: Joi.string().max(256).required().example('example').description('Template ID')
117
116
  }).label('CreateTemplateResponse'),
118
117
  failAction: 'log'
@@ -135,15 +134,7 @@ async function init(args) {
135
134
 
136
135
  return await templates.update(request.params.template, meta, request.payload.content);
137
136
  } catch (err) {
138
- request.logger.error({ msg: 'API request failed', err });
139
- if (Boom.isBoom(err)) {
140
- throw err;
141
- }
142
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
143
- if (err.code) {
144
- error.output.payload.code = err.code;
145
- }
146
- throw error;
137
+ handleError(request, err);
147
138
  }
148
139
  },
149
140
 
@@ -152,7 +143,11 @@ async function init(args) {
152
143
  notes: 'Update a stored template.',
153
144
  tags: ['api', 'Templates'],
154
145
 
155
- plugins: {},
146
+ plugins: {
147
+ 'hapi-swagger': {
148
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
149
+ }
150
+ },
156
151
 
157
152
  auth: {
158
153
  strategy: 'api-token',
@@ -198,7 +193,7 @@ async function init(args) {
198
193
  response: {
199
194
  schema: Joi.object({
200
195
  updated: Joi.boolean().description('Was the template updated or not'),
201
- account: accountIdSchema.required(),
196
+ account: templateAccountIdSchema,
202
197
  id: Joi.string().max(256).required().example('example').description('Template ID')
203
198
  }).label('UpdateTemplateResponse'),
204
199
  failAction: 'log'
@@ -220,15 +215,7 @@ async function init(args) {
220
215
 
221
216
  return await templates.list(request.query.account, request.query.page, request.query.pageSize);
222
217
  } catch (err) {
223
- request.logger.error({ msg: 'API request failed', err });
224
- if (Boom.isBoom(err)) {
225
- throw err;
226
- }
227
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
228
- if (err.code) {
229
- error.output.payload.code = err.code;
230
- }
231
- throw error;
218
+ handleError(request, err);
232
219
  }
233
220
  },
234
221
 
@@ -237,7 +224,11 @@ async function init(args) {
237
224
  notes: 'Lists stored templates for the account',
238
225
  tags: ['api', 'Templates'],
239
226
 
240
- plugins: {},
227
+ plugins: {
228
+ 'hapi-swagger': {
229
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
230
+ }
231
+ },
241
232
 
242
233
  auth: {
243
234
  strategy: 'api-token',
@@ -270,7 +261,7 @@ async function init(args) {
270
261
 
271
262
  response: {
272
263
  schema: Joi.object({
273
- account: accountIdSchema.required(),
264
+ account: templateAccountIdSchema,
274
265
  total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
275
266
  page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
276
267
  pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
@@ -308,17 +299,13 @@ async function init(args) {
308
299
 
309
300
  async handler(request) {
310
301
  try {
311
- return await templates.get(request.params.template);
312
- } catch (err) {
313
- request.logger.error({ msg: 'API request failed', err });
314
- if (Boom.isBoom(err)) {
315
- throw err;
302
+ let templateData = await templates.get(request.params.template);
303
+ if (!templateData) {
304
+ throwNotFound();
316
305
  }
317
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
318
- if (err.code) {
319
- error.output.payload.code = err.code;
320
- }
321
- throw error;
306
+ return templateData;
307
+ } catch (err) {
308
+ handleError(request, err);
322
309
  }
323
310
  },
324
311
 
@@ -327,7 +314,11 @@ async function init(args) {
327
314
  notes: 'Retrieve template content and information',
328
315
  tags: ['api', 'Templates'],
329
316
 
330
- plugins: {},
317
+ plugins: {
318
+ 'hapi-swagger': {
319
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
320
+ }
321
+ },
331
322
 
332
323
  auth: {
333
324
  strategy: 'api-token',
@@ -349,7 +340,7 @@ async function init(args) {
349
340
 
350
341
  response: {
351
342
  schema: Joi.object({
352
- account: accountIdSchema.required(),
343
+ account: templateAccountIdSchema,
353
344
  id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Template ID'),
354
345
  name: Joi.string().max(256).example('Transaction receipt').description('Name of the template').label('TemplateName').required(),
355
346
  description: Joi.string()
@@ -369,12 +360,7 @@ async function init(args) {
369
360
  subject: templateSchemas.subject,
370
361
  text: templateSchemas.text,
371
362
  html: templateSchemas.html,
372
- previewText: templateSchemas.previewText,
373
- format: Joi.string()
374
- .valid('html', 'markdown')
375
- .default('html')
376
- .description('Markup language for HTML ("html" or "markdown")')
377
- .label('TemplateContentFormat')
363
+ previewText: templateSchemas.previewText
378
364
  }).label('RequestTemplateContent')
379
365
  }).label('AccountTemplateResponse'),
380
366
  failAction: 'log'
@@ -390,15 +376,7 @@ async function init(args) {
390
376
  try {
391
377
  return await templates.del(request.params.template);
392
378
  } catch (err) {
393
- request.logger.error({ msg: 'API request failed', err });
394
- if (Boom.isBoom(err)) {
395
- throw err;
396
- }
397
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
398
- if (err.code) {
399
- error.output.payload.code = err.code;
400
- }
401
- throw error;
379
+ handleError(request, err);
402
380
  }
403
381
  },
404
382
  options: {
@@ -406,7 +384,11 @@ async function init(args) {
406
384
  notes: 'Delete a stored template',
407
385
  tags: ['api', 'Templates'],
408
386
 
409
- plugins: {},
387
+ plugins: {
388
+ 'hapi-swagger': {
389
+ responses: errorResponses(400, 401, 403, 429, 500)
390
+ }
391
+ },
410
392
 
411
393
  auth: {
412
394
  strategy: 'api-token',
@@ -430,7 +412,7 @@ async function init(args) {
430
412
  response: {
431
413
  schema: Joi.object({
432
414
  deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the template deleted'),
433
- account: accountIdSchema.required(),
415
+ account: templateAccountIdSchema,
434
416
  id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Template ID')
435
417
  }).label('DeleteTemplateResponse'),
436
418
  failAction: 'log'
@@ -446,20 +428,19 @@ async function init(args) {
446
428
  let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
447
429
 
448
430
  try {
431
+ if (!request.query.force) {
432
+ let err = new Error('Set the force=true query parameter to flush account templates');
433
+ err.code = 'ForceRequired';
434
+ err.statusCode = 400;
435
+ throw err;
436
+ }
437
+
449
438
  // throws if account does not exist
450
439
  await accountObject.loadAccountData();
451
440
 
452
441
  return await templates.flush(request.params.account);
453
442
  } catch (err) {
454
- request.logger.error({ msg: 'API request failed', err });
455
- if (Boom.isBoom(err)) {
456
- throw err;
457
- }
458
- let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
459
- if (err.code) {
460
- error.output.payload.code = err.code;
461
- }
462
- throw error;
443
+ handleError(request, err);
463
444
  }
464
445
  },
465
446
  options: {
@@ -467,7 +448,11 @@ async function init(args) {
467
448
  notes: 'Delete all stored templates for an account',
468
449
  tags: ['api', 'Templates'],
469
450
 
470
- plugins: {},
451
+ plugins: {
452
+ 'hapi-swagger': {
453
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
454
+ }
455
+ },
471
456
 
472
457
  auth: {
473
458
  strategy: 'api-token',
@@ -499,7 +484,7 @@ async function init(args) {
499
484
 
500
485
  response: {
501
486
  schema: Joi.object({
502
- deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Were the templates flushed'),
487
+ flushed: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Were the templates flushed'),
503
488
  account: accountIdSchema.required()
504
489
  }).label('FlushTemplatesResponse'),
505
490
  failAction: 'log'
@@ -0,0 +1,297 @@
1
+ 'use strict';
2
+
3
+ const Joi = require('joi');
4
+ const { redis } = require('../db');
5
+ const { Account } = require('../account');
6
+ const getSecret = require('../get-secret');
7
+ const tokens = require('../tokens');
8
+ const { failAction } = require('../tools');
9
+ const { handleError } = require('./route-helpers');
10
+ const { accountIdSchema, tokenRestrictionsSchema, ipSchema, tokenIdSchema, errorResponses } = require('../schemas');
11
+
12
+ const tokenAccessSchema = Joi.object({
13
+ time: Joi.date().iso().allow(null).example('2021-02-17T13:43:18.860Z').description('Last time this token was used. Null if the token has never been used'),
14
+ ip: ipSchema.allow(null).description('IP address of the last request that used this token. Null if the address was not available')
15
+ })
16
+ .unknown()
17
+ .description('Token usage information')
18
+ .label('TokenAccess');
19
+
20
+ const tokenScopesSchema = Joi.array()
21
+ .items(Joi.string().example('api').label('TokenScopeEntry'))
22
+ .description('Scopes this token is valid for')
23
+ .label('TokenScopes');
24
+
25
+ const tokenMetadataSchema = Joi.string()
26
+ .empty('')
27
+ .max(1024 * 1024)
28
+ .custom((value, helpers) => {
29
+ try {
30
+ // check if parsing fails
31
+ JSON.parse(value);
32
+ return value;
33
+ } catch (err) {
34
+ return helpers.message('Metadata must be a valid JSON string');
35
+ }
36
+ })
37
+ .example('{"example": "value"}')
38
+ .description('Related metadata in JSON format')
39
+ .label('JsonMetaData');
40
+
41
+ // Both token listings (root and account) return the same item shape from tokens.list(),
42
+ // the account listing just adds the account ID
43
+ const tokenListItemFields = {
44
+ description: Joi.string().empty('').trim().max(1024).example('Token description').description('Token description'),
45
+ metadata: tokenMetadataSchema,
46
+ ip: ipSchema.description('IP address of the requester').label('TokenIP'),
47
+ remoteAddress: ipSchema.description('IP address of the client that created the token').label('TokenRemoteAddress'),
48
+ scopes: tokenScopesSchema,
49
+ restrictions: tokenRestrictionsSchema,
50
+ created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this token was created'),
51
+ access: tokenAccessSchema,
52
+ id: tokenIdSchema
53
+ };
54
+
55
+ async function init(args) {
56
+ const { server, call, CORS_CONFIG } = args;
57
+
58
+ server.route({
59
+ method: 'POST',
60
+ path: '/v1/token',
61
+
62
+ async handler(request) {
63
+ let accountObject = new Account({
64
+ redis,
65
+ account: request.payload.account,
66
+ call,
67
+ secret: await getSecret(),
68
+ timeout: request.headers['x-ee-timeout']
69
+ });
70
+
71
+ try {
72
+ // throws if account does not exist
73
+ await accountObject.loadAccountData();
74
+
75
+ let token = await tokens.provision(Object.assign({}, request.payload, { remoteAddress: request.app.ip }));
76
+
77
+ return { token };
78
+ } catch (err) {
79
+ handleError(request, err);
80
+ }
81
+ },
82
+
83
+ options: {
84
+ description: 'Provision an access token',
85
+ notes: 'Provisions a new access token for an account',
86
+ tags: ['api', 'Access Tokens'],
87
+
88
+ plugins: {
89
+ 'hapi-swagger': {
90
+ responses: errorResponses(400, 401, 403, 404, 429, 500)
91
+ }
92
+ },
93
+
94
+ auth: {
95
+ strategy: 'api-token',
96
+ mode: 'required'
97
+ },
98
+ cors: CORS_CONFIG,
99
+
100
+ validate: {
101
+ options: {
102
+ stripUnknown: false,
103
+ abortEarly: false,
104
+ convert: true
105
+ },
106
+ failAction,
107
+
108
+ payload: Joi.object({
109
+ account: accountIdSchema.required(),
110
+
111
+ description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
112
+
113
+ scopes: Joi.array()
114
+ .items(Joi.string().valid('api', 'smtp', 'imap-proxy').label('TokenScope'))
115
+ .single()
116
+ .default(['api'])
117
+ .required()
118
+ .description(
119
+ 'Token permission scopes: "api" for REST API access, "smtp" for SMTP submission, "imap-proxy" for IMAP proxy authentication'
120
+ )
121
+ .label('Scopes'),
122
+
123
+ metadata: tokenMetadataSchema,
124
+
125
+ restrictions: tokenRestrictionsSchema,
126
+
127
+ ip: ipSchema.description('IP address of the requester').label('TokenIP')
128
+ }).label('CreateToken')
129
+ },
130
+
131
+ response: {
132
+ schema: Joi.object({
133
+ token: Joi.string().length(64).hex().required().example('123456').description('Access token')
134
+ }).label('CreateTokenResponse'),
135
+ failAction: 'log'
136
+ }
137
+ }
138
+ });
139
+
140
+ server.route({
141
+ method: 'DELETE',
142
+ path: '/v1/token/{token}',
143
+
144
+ async handler(request) {
145
+ try {
146
+ return { deleted: await tokens.delete(request.params.token, { remoteAddress: request.app.ip }) };
147
+ } catch (err) {
148
+ handleError(request, err);
149
+ }
150
+ },
151
+ options: {
152
+ description: 'Remove a token',
153
+ notes: 'Delete an access token',
154
+ tags: ['api', 'Access Tokens'],
155
+
156
+ plugins: {
157
+ 'hapi-swagger': {
158
+ responses: errorResponses(400, 401, 403, 429, 500)
159
+ }
160
+ },
161
+
162
+ auth: {
163
+ strategy: 'api-token',
164
+ mode: 'required'
165
+ },
166
+ cors: CORS_CONFIG,
167
+
168
+ validate: {
169
+ options: {
170
+ stripUnknown: false,
171
+ abortEarly: false,
172
+ convert: true
173
+ },
174
+ failAction,
175
+
176
+ params: Joi.object({
177
+ token: Joi.string().length(64).hex().required().example('123456').description('Access token')
178
+ }).label('DeleteTokenRequest')
179
+ },
180
+
181
+ response: {
182
+ schema: Joi.object({
183
+ deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the token deleted')
184
+ }).label('DeleteTokenRequestResponse'),
185
+ failAction: 'log'
186
+ }
187
+ }
188
+ });
189
+
190
+ server.route({
191
+ method: 'GET',
192
+ path: '/v1/tokens',
193
+
194
+ async handler(request) {
195
+ try {
196
+ // TODO: allow paging
197
+ return { tokens: (await tokens.list(null, 0, 1000)).tokens };
198
+ } catch (err) {
199
+ handleError(request, err);
200
+ }
201
+ },
202
+
203
+ options: {
204
+ description: 'List root tokens',
205
+ notes: 'Lists access tokens registered for root access',
206
+ tags: ['api', 'Access Tokens'],
207
+
208
+ plugins: {
209
+ 'hapi-swagger': {
210
+ responses: errorResponses(401, 403, 429, 500)
211
+ }
212
+ },
213
+
214
+ auth: {
215
+ strategy: 'api-token',
216
+ mode: 'required'
217
+ },
218
+ cors: CORS_CONFIG,
219
+
220
+ validate: {
221
+ options: {
222
+ stripUnknown: false,
223
+ abortEarly: false,
224
+ convert: true
225
+ },
226
+ failAction
227
+ },
228
+
229
+ response: {
230
+ schema: Joi.object({
231
+ tokens: Joi.array().items(Joi.object(tokenListItemFields).label('RootTokensItem')).label('RootTokensEntries')
232
+ }).label('RootTokensResponse'),
233
+ failAction: 'log'
234
+ }
235
+ }
236
+ });
237
+
238
+ server.route({
239
+ method: 'GET',
240
+ path: '/v1/tokens/account/{account}',
241
+
242
+ async handler(request) {
243
+ try {
244
+ // TODO: allow paging
245
+ return { tokens: (await tokens.list(request.params.account, 0, 1000)).tokens };
246
+ } catch (err) {
247
+ handleError(request, err);
248
+ }
249
+ },
250
+
251
+ options: {
252
+ description: 'List account tokens',
253
+ notes: 'Lists access tokens registered for an account',
254
+ tags: ['api', 'Access Tokens'],
255
+
256
+ plugins: {
257
+ 'hapi-swagger': {
258
+ responses: errorResponses(400, 401, 403, 429, 500)
259
+ }
260
+ },
261
+
262
+ auth: {
263
+ strategy: 'api-token',
264
+ mode: 'required'
265
+ },
266
+ cors: CORS_CONFIG,
267
+
268
+ validate: {
269
+ options: {
270
+ stripUnknown: false,
271
+ abortEarly: false,
272
+ convert: true
273
+ },
274
+ failAction,
275
+ params: Joi.object({
276
+ account: accountIdSchema.required()
277
+ })
278
+ },
279
+
280
+ response: {
281
+ schema: Joi.object({
282
+ tokens: Joi.array()
283
+ .items(
284
+ Joi.object({
285
+ account: accountIdSchema.required(),
286
+ ...tokenListItemFields
287
+ }).label('AccountTokensItem')
288
+ )
289
+ .label('AccountTokensEntries')
290
+ }).label('AccountsTokensResponse'),
291
+ failAction: 'log'
292
+ }
293
+ }
294
+ });
295
+ }
296
+
297
+ module.exports = init;