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.
- package/.github/workflows/deploy.yml +8 -3
- package/.github/workflows/release.yaml +6 -0
- package/CHANGELOG.md +59 -0
- package/Gruntfile.js +3 -1
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -1
- package/getswagger.sh +40 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +135 -72
- package/lib/api-routes/account-routes.js +684 -106
- package/lib/api-routes/blocklist-routes.js +344 -0
- package/lib/api-routes/chat-routes.js +32 -14
- package/lib/api-routes/delivery-test-routes.js +346 -0
- package/lib/api-routes/export-routes.js +28 -14
- package/lib/api-routes/gateway-routes.js +427 -0
- package/lib/api-routes/license-routes.js +156 -0
- package/lib/api-routes/mailbox-routes.js +344 -0
- package/lib/api-routes/message-routes.js +221 -187
- package/lib/api-routes/oauth2-app-routes.js +697 -0
- package/lib/api-routes/outbox-routes.js +185 -0
- package/lib/api-routes/pubsub-routes.js +102 -0
- package/lib/api-routes/route-helpers.js +58 -0
- package/lib/api-routes/settings-routes.js +357 -0
- package/lib/api-routes/stats-routes.js +111 -0
- package/lib/api-routes/submit-routes.js +461 -0
- package/lib/api-routes/template-routes.js +60 -75
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +181 -0
- package/lib/autodetect-imap-settings.js +0 -2
- package/lib/consts.js +5 -0
- package/lib/email-client/base-client.js +28 -6
- package/lib/email-client/gmail-client.js +133 -112
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -13
- package/lib/email-client/imap/sync-operations.js +131 -3
- package/lib/email-client/imap-client.js +152 -75
- package/lib/email-client/notification-handler.js +1 -4
- package/lib/email-client/outlook-client.js +134 -75
- package/lib/export.js +97 -20
- package/lib/feature-flags.js +2 -2
- package/lib/gateway.js +4 -9
- package/lib/get-raw-email.js +5 -5
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
- package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/logger.js +24 -21
- package/lib/message-port-stream.js +113 -16
- package/lib/metrics-collector.js +0 -2
- package/lib/oauth2-apps.js +13 -4
- package/lib/outbox.js +24 -40
- package/lib/redis-operations.js +1 -1
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +429 -84
- package/lib/sentry.js +139 -0
- package/lib/settings.js +9 -3
- package/lib/stream-encrypt.js +1 -1
- package/lib/templates.js +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tools.js +70 -4
- package/lib/ui-routes/account-routes.js +45 -212
- package/lib/ui-routes/admin-config-routes.js +928 -489
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
- package/lib/ui-routes/route-helpers.js +314 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +232 -0
- package/lib/webhook-request.js +36 -0
- package/lib/webhooks.js +8 -4
- package/package.json +13 -12
- package/sbom.json +1 -1
- package/server.js +222 -39
- package/static/licenses.html +160 -300
- package/translations/messages.pot +112 -132
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +200 -4424
- package/workers/documents.js +2 -22
- package/workers/export.js +103 -104
- package/workers/imap-proxy.js +3 -23
- package/workers/imap.js +32 -36
- package/workers/smtp.js +2 -22
- package/workers/submit.js +26 -35
- package/workers/webhooks.js +9 -43
|
@@ -1,34 +1,51 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Admin UI routes for the remaining /admin/config/* pages: webhooks, service URL/branding,
|
|
4
|
+
// AI (OpenAI) settings, logging, and license. Extracted verbatim from lib/routes-ui.js.
|
|
5
|
+
// The notificationTypes / configWebhooksSchema / configLoggingSchema consts, the
|
|
6
|
+
// getOpenAiError helper, and the getDefaultPrompt helper move with the routes (only these
|
|
7
|
+
// config pages use them).
|
|
8
|
+
|
|
3
9
|
const Joi = require('joi');
|
|
4
10
|
const crypto = require('crypto');
|
|
11
|
+
const config = require('@zone-eu/wild-config');
|
|
12
|
+
const libmime = require('libmime');
|
|
5
13
|
const he = require('he');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const packageData = require('../../package.json');
|
|
6
16
|
const { simpleParser } = require('mailparser');
|
|
7
|
-
const
|
|
17
|
+
const { fetch: fetchCmd } = require('undici');
|
|
8
18
|
|
|
9
19
|
const settings = require('../settings');
|
|
10
|
-
const
|
|
20
|
+
const consts = require('../consts');
|
|
11
21
|
const getSecret = require('../get-secret');
|
|
22
|
+
const timezonesList = require('timezones-list').default;
|
|
23
|
+
const { redis, submitQueue, notifyQueue, documentsQueue } = require('../db');
|
|
24
|
+
const { getByteSize, formatByteSize, getDuration, failAction, hasEnvValue, readEnvValue, httpAgent } = require('../tools');
|
|
12
25
|
const { llmPreProcess } = require('../llm-pre-process');
|
|
13
26
|
const { locales } = require('../translations');
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const timezonesList = require('timezones-list').default;
|
|
27
|
+
const { settingsSchema } = require('../schemas');
|
|
28
|
+
const { getOpenAiModels, OPEN_AI_MODELS, getExampleDocumentsPayloads } = require('./route-helpers');
|
|
17
29
|
|
|
18
|
-
const {
|
|
30
|
+
const { REDIS_PREFIX, DEFAULT_MAX_LOG_LINES, DEFAULT_DELIVERY_ATTEMPTS, DEFAULT_MAX_BODY_SIZE, DEFAULT_MAX_PAYLOAD_TIMEOUT, NONCE_BYTES } = consts;
|
|
19
31
|
|
|
20
|
-
const
|
|
32
|
+
const LICENSE_HOST = 'https://postalsys.com';
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
config.api = config.api || {
|
|
35
|
+
port: 3000,
|
|
36
|
+
host: '127.0.0.1',
|
|
37
|
+
proxy: false
|
|
38
|
+
};
|
|
24
39
|
|
|
25
|
-
const
|
|
40
|
+
const MAX_BODY_SIZE = getByteSize(readEnvValue('EENGINE_MAX_BODY_SIZE') || config.api.maxBodySize) || DEFAULT_MAX_BODY_SIZE;
|
|
41
|
+
const MAX_PAYLOAD_TIMEOUT = getDuration(readEnvValue('EENGINE_MAX_PAYLOAD_TIMEOUT') || config.api.maxPayloadTimeout) || DEFAULT_MAX_PAYLOAD_TIMEOUT;
|
|
26
42
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
const ADMIN_ACCESS_ADDRESSES = hasEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
44
|
+
? readEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
45
|
+
.split(',')
|
|
46
|
+
.map(v => v.trim())
|
|
47
|
+
.filter(v => v)
|
|
48
|
+
: null;
|
|
32
49
|
|
|
33
50
|
const IMAP_INDEXERS = [
|
|
34
51
|
{
|
|
@@ -55,30 +72,25 @@ const notificationTypes = Object.keys(consts)
|
|
|
55
72
|
description: consts[`${key}_DESCRIPTION`]
|
|
56
73
|
}));
|
|
57
74
|
|
|
58
|
-
const ADMIN_ACCESS_ADDRESSES = hasEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
59
|
-
? readEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
60
|
-
.split(',')
|
|
61
|
-
.map(v => v.trim())
|
|
62
|
-
.filter(v => v)
|
|
63
|
-
: null;
|
|
64
|
-
|
|
65
|
-
const MAX_BODY_SIZE = getByteSize(readEnvValue('EENGINE_MAX_BODY_SIZE')) || consts.DEFAULT_MAX_BODY_SIZE;
|
|
66
|
-
const MAX_PAYLOAD_TIMEOUT = getDuration(readEnvValue('EENGINE_MAX_PAYLOAD_TIMEOUT')) || consts.DEFAULT_MAX_PAYLOAD_TIMEOUT;
|
|
67
|
-
|
|
68
|
-
// Validation schemas
|
|
69
75
|
const configWebhooksSchema = {
|
|
70
|
-
webhooksEnabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
76
|
+
webhooksEnabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false).description('Enable Webhooks'),
|
|
71
77
|
webhooks: Joi.string()
|
|
72
|
-
.uri({
|
|
78
|
+
.uri({
|
|
79
|
+
scheme: ['http', 'https'],
|
|
80
|
+
allowRelative: false
|
|
81
|
+
})
|
|
73
82
|
.allow('')
|
|
74
|
-
.example('https://myservice.com/imap/webhooks')
|
|
83
|
+
.example('https://myservice.com/imap/webhooks')
|
|
84
|
+
.description('Webhook URL'),
|
|
75
85
|
notifyAll: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
76
86
|
headersAll: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
77
87
|
notifyHeaders: Joi.string().empty('').trim(),
|
|
78
88
|
notifyText: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
79
89
|
notifyWebSafeHtml: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
90
|
+
|
|
80
91
|
notifyTextSize: Joi.alternatives().try(
|
|
81
92
|
Joi.number().empty('').integer().min(0),
|
|
93
|
+
// If it's a string, parse and convert it to bytes
|
|
82
94
|
Joi.string().custom((value, helpers) => {
|
|
83
95
|
let nr = getByteSize(value);
|
|
84
96
|
if (typeof nr !== 'number' || nr < 0) {
|
|
@@ -87,11 +99,14 @@ const configWebhooksSchema = {
|
|
|
87
99
|
return nr;
|
|
88
100
|
}, 'Byte size conversion')
|
|
89
101
|
),
|
|
102
|
+
|
|
90
103
|
notifyCalendarEvents: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
91
104
|
inboxNewOnly: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
105
|
+
|
|
92
106
|
notifyAttachments: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
93
107
|
notifyAttachmentSize: Joi.alternatives().try(
|
|
94
108
|
Joi.number().empty('').integer().min(0),
|
|
109
|
+
// If it's a string, parse and convert it to bytes
|
|
95
110
|
Joi.string().custom((value, helpers) => {
|
|
96
111
|
let nr = getByteSize(value);
|
|
97
112
|
if (typeof nr !== 'number' || nr < 0) {
|
|
@@ -100,10 +115,12 @@ const configWebhooksSchema = {
|
|
|
100
115
|
return nr;
|
|
101
116
|
}, 'Byte size conversion')
|
|
102
117
|
),
|
|
118
|
+
|
|
103
119
|
customHeaders: Joi.string()
|
|
104
120
|
.allow('')
|
|
105
121
|
.trim()
|
|
106
122
|
.max(10 * 1024)
|
|
123
|
+
.description('Custom request headers')
|
|
107
124
|
};
|
|
108
125
|
|
|
109
126
|
for (let type of notificationTypes) {
|
|
@@ -111,47 +128,27 @@ for (let type of notificationTypes) {
|
|
|
111
128
|
}
|
|
112
129
|
|
|
113
130
|
const configLoggingSchema = {
|
|
114
|
-
all: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
115
|
-
maxLogLines: Joi.number().integer().empty('').min(0).max(10000000).default(DEFAULT_MAX_LOG_LINES)
|
|
131
|
+
all: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false).description('Enable logs for all accounts'),
|
|
132
|
+
maxLogLines: Joi.number().integer().empty('').min(0).max(10000000).default(DEFAULT_MAX_LOG_LINES),
|
|
133
|
+
sentryEnabled: settingsSchema.sentryEnabled.default(false),
|
|
134
|
+
sentryDsn: settingsSchema.sentryDsn.default('')
|
|
116
135
|
};
|
|
117
136
|
|
|
118
|
-
// Helper functions
|
|
119
|
-
async function getOpenAiModels(models, selectedModel) {
|
|
120
|
-
let modelList = (await settings.get('openAiModels')) || structuredClone(models);
|
|
121
|
-
|
|
122
|
-
if (selectedModel && !modelList.find(model => model.id === selectedModel)) {
|
|
123
|
-
modelList.unshift({ name: selectedModel, id: selectedModel });
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return modelList.map(model => {
|
|
127
|
-
model.selected = model.id === selectedModel;
|
|
128
|
-
return model;
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
137
|
async function getOpenAiError(gt) {
|
|
133
|
-
let
|
|
134
|
-
if (
|
|
138
|
+
let openAiError = await redis.get(`${REDIS_PREFIX}:openai:error`);
|
|
139
|
+
if (openAiError) {
|
|
135
140
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
openAiError = JSON.parse(openAiError);
|
|
142
|
+
switch (openAiError.code) {
|
|
143
|
+
case 'invalid_api_key':
|
|
144
|
+
openAiError.message = gt.gettext('Invalid API key for OpenAI');
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
138
147
|
} catch (err) {
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function getExampleDocumentsPayloads() {
|
|
146
|
-
const exampleDocumentsPayloads = require('../payload-examples-documents.json');
|
|
147
|
-
let examples = structuredClone(exampleDocumentsPayloads);
|
|
148
|
-
let serviceUrl = await settings.get('serviceUrl');
|
|
149
|
-
for (let example of examples) {
|
|
150
|
-
if (example.serviceUrl) {
|
|
151
|
-
example.serviceUrl = serviceUrl || example.serviceUrl;
|
|
148
|
+
openAiError = null;
|
|
152
149
|
}
|
|
153
150
|
}
|
|
154
|
-
return
|
|
151
|
+
return openAiError;
|
|
155
152
|
}
|
|
156
153
|
|
|
157
154
|
function init(args) {
|
|
@@ -162,7 +159,6 @@ function init(args) {
|
|
|
162
159
|
cmd: 'openAiDefaultPrompt'
|
|
163
160
|
});
|
|
164
161
|
|
|
165
|
-
// Webhooks config routes
|
|
166
162
|
server.route({
|
|
167
163
|
method: 'GET',
|
|
168
164
|
path: '/admin/config/webhooks',
|
|
@@ -173,8 +169,10 @@ function init(args) {
|
|
|
173
169
|
const notifyWebSafeHtml = (await settings.get('notifyWebSafeHtml')) || false;
|
|
174
170
|
const notifyTextSize = Number(await settings.get('notifyTextSize')) || 0;
|
|
175
171
|
const notifyCalendarEvents = (await settings.get('notifyCalendarEvents')) || false;
|
|
172
|
+
|
|
176
173
|
const notifyAttachments = (await settings.get('notifyAttachments')) || false;
|
|
177
174
|
const notifyAttachmentSize = Number(await settings.get('notifyAttachmentSize')) || 0;
|
|
175
|
+
|
|
178
176
|
const inboxNewOnly = (await settings.get('inboxNewOnly')) || false;
|
|
179
177
|
const customHeaders = (await settings.get('webhooksCustomHeaders')) || [];
|
|
180
178
|
|
|
@@ -182,8 +180,10 @@ function init(args) {
|
|
|
182
180
|
let values = {
|
|
183
181
|
webhooksEnabled: webhooksEnabled !== null ? !!webhooksEnabled : false,
|
|
184
182
|
webhooks: (await settings.get('webhooks')) || '',
|
|
183
|
+
|
|
185
184
|
notifyAll: webhookEvents.includes('*'),
|
|
186
185
|
inboxNewOnly,
|
|
186
|
+
|
|
187
187
|
headersAll: notifyHeaders.includes('*'),
|
|
188
188
|
notifyHeaders: notifyHeaders
|
|
189
189
|
.filter(entry => entry !== '*')
|
|
@@ -193,8 +193,10 @@ function init(args) {
|
|
|
193
193
|
notifyWebSafeHtml,
|
|
194
194
|
notifyTextSize: notifyTextSize ? formatByteSize(notifyTextSize) : '',
|
|
195
195
|
notifyCalendarEvents,
|
|
196
|
+
|
|
196
197
|
notifyAttachments,
|
|
197
198
|
notifyAttachmentSize: notifyAttachmentSize ? formatByteSize(notifyAttachmentSize) : '',
|
|
199
|
+
|
|
198
200
|
customHeaders: []
|
|
199
201
|
.concat(customHeaders || [])
|
|
200
202
|
.map(entry => `${entry.key}: ${entry.value}`.trim())
|
|
@@ -207,14 +209,19 @@ function init(args) {
|
|
|
207
209
|
pageTitle: 'Webhooks',
|
|
208
210
|
menuConfig: true,
|
|
209
211
|
menuConfigWebhooks: true,
|
|
212
|
+
|
|
210
213
|
notificationTypes: notificationTypes.map(type =>
|
|
211
214
|
Object.assign({}, type, { checked: webhookEvents.includes(type.name), isMessageNew: type.name === 'messageNew' })
|
|
212
215
|
),
|
|
216
|
+
|
|
213
217
|
values,
|
|
218
|
+
|
|
214
219
|
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
215
220
|
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
216
221
|
},
|
|
217
|
-
{
|
|
222
|
+
{
|
|
223
|
+
layout: 'app'
|
|
224
|
+
}
|
|
218
225
|
);
|
|
219
226
|
}
|
|
220
227
|
});
|
|
@@ -231,9 +238,15 @@ function init(args) {
|
|
|
231
238
|
.map(line => {
|
|
232
239
|
let sep = line.indexOf(':');
|
|
233
240
|
if (sep >= 0) {
|
|
234
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
key: line.substring(0, sep).trim(),
|
|
243
|
+
value: line.substring(sep + 1).trim()
|
|
244
|
+
};
|
|
235
245
|
}
|
|
236
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
key: line,
|
|
248
|
+
value: ''
|
|
249
|
+
};
|
|
237
250
|
});
|
|
238
251
|
|
|
239
252
|
const data = {
|
|
@@ -243,14 +256,18 @@ function init(args) {
|
|
|
243
256
|
notifyWebSafeHtml: request.payload.notifyWebSafeHtml,
|
|
244
257
|
notifyTextSize: request.payload.notifyTextSize || 0,
|
|
245
258
|
notifyCalendarEvents: request.payload.notifyCalendarEvents,
|
|
259
|
+
|
|
246
260
|
notifyAttachments: request.payload.notifyAttachments,
|
|
247
261
|
notifyAttachmentSize: request.payload.notifyAttachmentSize,
|
|
262
|
+
|
|
248
263
|
inboxNewOnly: request.payload.inboxNewOnly,
|
|
264
|
+
|
|
249
265
|
webhookEvents: notificationTypes.filter(type => !!request.payload[`notify_${type.name}`]).map(type => type.name),
|
|
250
266
|
notifyHeaders: (request.payload.notifyHeaders || '')
|
|
251
267
|
.split(/\r?\n/)
|
|
252
268
|
.map(line => line.toLowerCase().trim())
|
|
253
269
|
.filter(line => line),
|
|
270
|
+
|
|
254
271
|
webhooksCustomHeaders: customHeaders
|
|
255
272
|
};
|
|
256
273
|
|
|
@@ -267,10 +284,12 @@ function init(args) {
|
|
|
267
284
|
}
|
|
268
285
|
|
|
269
286
|
if (!data.webhooksEnabled) {
|
|
287
|
+
// clear error message (if exists)
|
|
270
288
|
await settings.clear('webhookErrorFlag');
|
|
271
289
|
}
|
|
272
290
|
|
|
273
291
|
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
292
|
+
|
|
274
293
|
return h.redirect('/admin/config/webhooks');
|
|
275
294
|
} catch (err) {
|
|
276
295
|
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
@@ -282,21 +301,31 @@ function init(args) {
|
|
|
282
301
|
pageTitle: 'Webhooks',
|
|
283
302
|
menuConfig: true,
|
|
284
303
|
menuConfigWebhooks: true,
|
|
304
|
+
|
|
285
305
|
notificationTypes: notificationTypes.map(type =>
|
|
286
306
|
Object.assign({}, type, { checked: !!request.payload[`notify_${type.name}`], isMessageNew: type.name === 'messageNew' })
|
|
287
307
|
),
|
|
308
|
+
|
|
288
309
|
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
289
310
|
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
290
311
|
},
|
|
291
|
-
{
|
|
312
|
+
{
|
|
313
|
+
layout: 'app'
|
|
314
|
+
}
|
|
292
315
|
);
|
|
293
316
|
}
|
|
294
317
|
},
|
|
295
318
|
options: {
|
|
296
319
|
validate: {
|
|
297
|
-
options: {
|
|
320
|
+
options: {
|
|
321
|
+
stripUnknown: true,
|
|
322
|
+
abortEarly: false,
|
|
323
|
+
convert: true
|
|
324
|
+
},
|
|
325
|
+
|
|
298
326
|
async failAction(request, h, err) {
|
|
299
327
|
let errors = {};
|
|
328
|
+
|
|
300
329
|
if (err.details) {
|
|
301
330
|
err.details.forEach(detail => {
|
|
302
331
|
if (!errors[detail.path]) {
|
|
@@ -315,6 +344,7 @@ function init(args) {
|
|
|
315
344
|
pageTitle: 'Webhooks',
|
|
316
345
|
menuConfig: true,
|
|
317
346
|
menuConfigWebhooks: true,
|
|
347
|
+
|
|
318
348
|
notificationTypes: notificationTypes.map(type =>
|
|
319
349
|
Object.assign({}, type, {
|
|
320
350
|
checked: !!request.payload[`notify_${type.name}`],
|
|
@@ -322,20 +352,24 @@ function init(args) {
|
|
|
322
352
|
error: errors[`notify_${type.name}`]
|
|
323
353
|
})
|
|
324
354
|
),
|
|
355
|
+
|
|
325
356
|
errors,
|
|
357
|
+
|
|
326
358
|
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
327
359
|
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
328
360
|
},
|
|
329
|
-
{
|
|
361
|
+
{
|
|
362
|
+
layout: 'app'
|
|
363
|
+
}
|
|
330
364
|
)
|
|
331
365
|
.takeover();
|
|
332
366
|
},
|
|
367
|
+
|
|
333
368
|
payload: Joi.object(configWebhooksSchema)
|
|
334
369
|
}
|
|
335
370
|
}
|
|
336
371
|
});
|
|
337
372
|
|
|
338
|
-
// Service config routes
|
|
339
373
|
server.route({
|
|
340
374
|
method: 'GET',
|
|
341
375
|
path: '/admin/config/service',
|
|
@@ -347,22 +381,26 @@ function init(args) {
|
|
|
347
381
|
serviceSecret: (await settings.get('serviceSecret')) || null,
|
|
348
382
|
queueKeep: (await settings.get('queueKeep')) || 0,
|
|
349
383
|
deliveryAttempts: await settings.get('deliveryAttempts'),
|
|
384
|
+
|
|
350
385
|
imapIndexer: (await settings.get('imapIndexer')) || 'full',
|
|
386
|
+
|
|
351
387
|
pageBrandName: (await settings.get('pageBrandName')) || '',
|
|
352
388
|
templateHeader: (await settings.get('templateHeader')) || '',
|
|
353
389
|
templateHtmlHead: (await settings.get('templateHtmlHead')) || '',
|
|
354
390
|
scriptEnv: (await settings.get('scriptEnv')) || '',
|
|
355
391
|
enableTokens: !(await settings.get('disableTokens')),
|
|
356
392
|
enableApiProxy: (await settings.get('enableApiProxy')) || false,
|
|
393
|
+
|
|
357
394
|
trackClicks: await settings.get('trackClicks'),
|
|
358
395
|
trackOpens: await settings.get('trackOpens'),
|
|
396
|
+
|
|
359
397
|
resolveGmailCategories: (await settings.get('resolveGmailCategories')) || false,
|
|
360
398
|
enableOAuthTokensApi: (await settings.get('enableOAuthTokensApi')) || false,
|
|
399
|
+
|
|
361
400
|
ignoreMailCertErrors: (await settings.get('ignoreMailCertErrors')) || false,
|
|
401
|
+
|
|
362
402
|
locale: (await settings.get('locale')) || false,
|
|
363
|
-
timezone: (await settings.get('timezone')) || false
|
|
364
|
-
gmailExportBatchSize: (await settings.get('gmailExportBatchSize')) || DEFAULT_GMAIL_EXPORT_BATCH_SIZE,
|
|
365
|
-
outlookExportBatchSize: (await settings.get('outlookExportBatchSize')) || DEFAULT_OUTLOOK_EXPORT_BATCH_SIZE
|
|
403
|
+
timezone: (await settings.get('timezone')) || false
|
|
366
404
|
};
|
|
367
405
|
|
|
368
406
|
if (typeof values.trackClicks !== 'boolean') {
|
|
@@ -385,21 +423,27 @@ function init(args) {
|
|
|
385
423
|
menuConfigService: true,
|
|
386
424
|
encryption: await getSecret(),
|
|
387
425
|
locales: locales.map(locale => Object.assign({ selected: locale.locale === values.locale }, locale)),
|
|
426
|
+
|
|
388
427
|
timezones: timezonesList.map(entry => ({
|
|
389
428
|
name: entry.label,
|
|
390
429
|
timezone: entry.tzCode,
|
|
391
430
|
selected: entry.tzCode === values.timezone
|
|
392
431
|
})),
|
|
432
|
+
|
|
393
433
|
imapIndexers: structuredClone(IMAP_INDEXERS).map(entry => {
|
|
394
434
|
if (entry.id === values.imapIndexer) {
|
|
395
435
|
entry.selected = true;
|
|
396
436
|
}
|
|
397
437
|
return entry;
|
|
398
438
|
}),
|
|
439
|
+
|
|
399
440
|
adminAccessLimit: ADMIN_ACCESS_ADDRESSES && ADMIN_ACCESS_ADDRESSES.length,
|
|
441
|
+
|
|
400
442
|
values
|
|
401
443
|
},
|
|
402
|
-
{
|
|
444
|
+
{
|
|
445
|
+
layout: 'app'
|
|
446
|
+
}
|
|
403
447
|
);
|
|
404
448
|
}
|
|
405
449
|
});
|
|
@@ -426,9 +470,7 @@ function init(args) {
|
|
|
426
470
|
locale: request.payload.locale,
|
|
427
471
|
timezone: request.payload.timezone,
|
|
428
472
|
deliveryAttempts: request.payload.deliveryAttempts,
|
|
429
|
-
imapIndexer: request.payload.imapIndexer
|
|
430
|
-
gmailExportBatchSize: request.payload.gmailExportBatchSize,
|
|
431
|
-
outlookExportBatchSize: request.payload.outlookExportBatchSize
|
|
473
|
+
imapIndexer: request.payload.imapIndexer
|
|
432
474
|
};
|
|
433
475
|
|
|
434
476
|
if (request.payload.serviceUrl) {
|
|
@@ -441,6 +483,7 @@ function init(args) {
|
|
|
441
483
|
}
|
|
442
484
|
|
|
443
485
|
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
486
|
+
|
|
444
487
|
return h.redirect('/admin/config/service');
|
|
445
488
|
} catch (err) {
|
|
446
489
|
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
@@ -454,28 +497,39 @@ function init(args) {
|
|
|
454
497
|
menuConfigService: true,
|
|
455
498
|
locales: locales.map(locale => Object.assign({ selected: locale.locale === request.payload.locale }, locale)),
|
|
456
499
|
encryption: await getSecret(),
|
|
500
|
+
|
|
457
501
|
timezones: timezonesList.map(entry => ({
|
|
458
502
|
name: entry.label,
|
|
459
503
|
timezone: entry.tzCode,
|
|
460
504
|
selected: entry.tzCode === request.payload.timezone
|
|
461
505
|
})),
|
|
506
|
+
|
|
462
507
|
imapIndexers: structuredClone(IMAP_INDEXERS).map(entry => {
|
|
463
508
|
if (entry.id === request.payload.imapIndexer) {
|
|
464
509
|
entry.selected = true;
|
|
465
510
|
}
|
|
466
511
|
return entry;
|
|
467
512
|
}),
|
|
513
|
+
|
|
468
514
|
adminAccessLimit: ADMIN_ACCESS_ADDRESSES && ADMIN_ACCESS_ADDRESSES.length
|
|
469
515
|
},
|
|
470
|
-
{
|
|
516
|
+
{
|
|
517
|
+
layout: 'app'
|
|
518
|
+
}
|
|
471
519
|
);
|
|
472
520
|
}
|
|
473
521
|
},
|
|
474
522
|
options: {
|
|
475
523
|
validate: {
|
|
476
|
-
options: {
|
|
524
|
+
options: {
|
|
525
|
+
stripUnknown: true,
|
|
526
|
+
abortEarly: false,
|
|
527
|
+
convert: true
|
|
528
|
+
},
|
|
529
|
+
|
|
477
530
|
async failAction(request, h, err) {
|
|
478
531
|
let errors = {};
|
|
532
|
+
|
|
479
533
|
if (err.details) {
|
|
480
534
|
err.details.forEach(detail => {
|
|
481
535
|
if (!errors[detail.path]) {
|
|
@@ -496,24 +550,31 @@ function init(args) {
|
|
|
496
550
|
menuConfigService: true,
|
|
497
551
|
locales: locales.map(locale => Object.assign({ selected: locale.locale === request.payload.locale }, locale)),
|
|
498
552
|
encryption: await getSecret(),
|
|
553
|
+
|
|
499
554
|
timezones: timezonesList.map(entry => ({
|
|
500
555
|
name: entry.label,
|
|
501
556
|
timezone: entry.tzCode,
|
|
502
557
|
selected: entry.tzCode === request.payload.timezone
|
|
503
558
|
})),
|
|
559
|
+
|
|
504
560
|
imapIndexers: structuredClone(IMAP_INDEXERS).map(entry => {
|
|
505
561
|
if (entry.id === request.payload.imapIndexer) {
|
|
506
562
|
entry.selected = true;
|
|
507
563
|
}
|
|
508
564
|
return entry;
|
|
509
565
|
}),
|
|
566
|
+
|
|
510
567
|
adminAccessLimit: ADMIN_ACCESS_ADDRESSES && ADMIN_ACCESS_ADDRESSES.length,
|
|
568
|
+
|
|
511
569
|
errors
|
|
512
570
|
},
|
|
513
|
-
{
|
|
571
|
+
{
|
|
572
|
+
layout: 'app'
|
|
573
|
+
}
|
|
514
574
|
)
|
|
515
575
|
.takeover();
|
|
516
576
|
},
|
|
577
|
+
|
|
517
578
|
payload: Joi.object({
|
|
518
579
|
serviceUrl: settingsSchema.serviceUrl,
|
|
519
580
|
serviceSecret: settingsSchema.serviceSecret,
|
|
@@ -529,154 +590,583 @@ function init(args) {
|
|
|
529
590
|
trackClicks: settingsSchema.trackClicks.default(false),
|
|
530
591
|
resolveGmailCategories: settingsSchema.resolveGmailCategories.default(false),
|
|
531
592
|
ignoreMailCertErrors: settingsSchema.ignoreMailCertErrors.default(false),
|
|
532
|
-
|
|
593
|
+
|
|
594
|
+
// Following options can only be changed via the UI
|
|
595
|
+
enableOAuthTokensApi: Joi.boolean()
|
|
596
|
+
.truthy('Y', 'true', '1', 'on')
|
|
597
|
+
.falsy('N', 'false', 0, '')
|
|
598
|
+
.description('If true, then allow using using the OAuth tokens API endpoint')
|
|
599
|
+
.default(false),
|
|
533
600
|
enableTokens: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
601
|
+
|
|
534
602
|
locale: settingsSchema.locale
|
|
535
603
|
.empty('')
|
|
536
604
|
.valid(...locales.map(locale => locale.locale))
|
|
537
605
|
.default('en'),
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
outlookExportBatchSize: settingsSchema.outlookExportBatchSize.default(DEFAULT_OUTLOOK_EXPORT_BATCH_SIZE)
|
|
606
|
+
|
|
607
|
+
timezone: settingsSchema.timezone.empty('')
|
|
541
608
|
})
|
|
542
609
|
}
|
|
543
610
|
}
|
|
544
611
|
});
|
|
545
612
|
|
|
546
|
-
// Service preview route
|
|
547
613
|
server.route({
|
|
548
|
-
method: '
|
|
549
|
-
path: '/admin/config/
|
|
614
|
+
method: 'GET',
|
|
615
|
+
path: '/admin/config/ai',
|
|
550
616
|
async handler(request, h) {
|
|
551
|
-
|
|
552
|
-
'
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
validate: {
|
|
563
|
-
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
564
|
-
async failAction(request, h, err) {
|
|
565
|
-
request.logger.error({ msg: 'Failed to process preview', err });
|
|
566
|
-
return h.redirect('/admin').takeover();
|
|
567
|
-
},
|
|
568
|
-
payload: Joi.object({
|
|
569
|
-
pageBrandName: settingsSchema.pageBrandName.default(''),
|
|
570
|
-
templateHeader: settingsSchema.templateHeader.default(''),
|
|
571
|
-
templateHtmlHead: settingsSchema.templateHtmlHead.default('')
|
|
572
|
-
})
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
});
|
|
617
|
+
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
618
|
+
if (entry.error && typeof entry.error === 'string') {
|
|
619
|
+
entry.error = entry.error
|
|
620
|
+
.replace(/\r?\n/g, '\n')
|
|
621
|
+
.replace(/^\s+at\s+.*$/gm, '')
|
|
622
|
+
.replace(/\n+/g, '\n')
|
|
623
|
+
.trim()
|
|
624
|
+
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
625
|
+
}
|
|
626
|
+
return entry;
|
|
627
|
+
});
|
|
576
628
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
break;
|
|
586
|
-
case 'webhook-default':
|
|
587
|
-
await settings.clear('webhookErrorFlag');
|
|
588
|
-
break;
|
|
589
|
-
case 'webhook-route':
|
|
590
|
-
if (request.payload.entry) {
|
|
591
|
-
await redis.hdel(`${REDIS_PREFIX}wh:c`, `${request.payload.entry}:webhookErrorFlag`);
|
|
592
|
-
}
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
return { success: true };
|
|
596
|
-
},
|
|
597
|
-
options: {
|
|
598
|
-
validate: {
|
|
599
|
-
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
600
|
-
failAction,
|
|
601
|
-
payload: Joi.object({
|
|
602
|
-
alert: Joi.string().required().max(1024),
|
|
603
|
-
entry: Joi.string().empty('').max(1024).trim()
|
|
604
|
-
})
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
});
|
|
629
|
+
const values = {
|
|
630
|
+
generateEmailSummary: (await settings.get('generateEmailSummary')) || false,
|
|
631
|
+
openAiPrompt: ((await settings.get('openAiPrompt')) || '').toString(),
|
|
632
|
+
contentFnJson: JSON.stringify(
|
|
633
|
+
((await settings.get(`openAiPreProcessingFn`)) || '').toString() ||
|
|
634
|
+
`// Pass all emails
|
|
635
|
+
return true;`
|
|
636
|
+
),
|
|
608
637
|
|
|
609
|
-
|
|
610
|
-
server.route({
|
|
611
|
-
method: 'POST',
|
|
612
|
-
path: '/admin/config/service/clean',
|
|
613
|
-
async handler(request) {
|
|
614
|
-
let errors = [];
|
|
615
|
-
for (let queue of [submitQueue, notifyQueue, documentsQueue]) {
|
|
616
|
-
for (let type of ['failed', 'completed']) {
|
|
617
|
-
try {
|
|
618
|
-
await queue.clean(1000, 100000, type);
|
|
619
|
-
request.logger.trace({ msg: 'Queue cleaned', queue: queue.name, type });
|
|
620
|
-
} catch (err) {
|
|
621
|
-
request.logger.error({ msg: 'Failed to clean queue', queue: queue.name, type, err });
|
|
622
|
-
errors.push(err.message);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
638
|
+
openAiAPIUrl: ((await settings.get('openAiAPIUrl')) || '').toString(),
|
|
626
639
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
},
|
|
632
|
-
options: {
|
|
633
|
-
validate: {
|
|
634
|
-
options: { stripUnknown: true, abortEarly: false, convert: true }
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
});
|
|
640
|
+
openAiTemperature: ((await settings.get('openAiTemperature')) || '').toString(),
|
|
641
|
+
openAiTopP: ((await settings.get('openAiTopP')) || '').toString(),
|
|
642
|
+
openAiMaxTokens: ((await settings.get('openAiMaxTokens')) || '').toString()
|
|
643
|
+
};
|
|
638
644
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
method: 'GET',
|
|
642
|
-
path: '/admin/config/logging',
|
|
643
|
-
async handler(request, h) {
|
|
644
|
-
let values = (await settings.get('logs')) || {};
|
|
645
|
-
if (typeof values.maxLogLines === 'undefined') {
|
|
646
|
-
values.maxLogLines = DEFAULT_MAX_LOG_LINES;
|
|
645
|
+
if (!values.openAiPrompt.trim()) {
|
|
646
|
+
values.openAiPrompt = await getDefaultPrompt();
|
|
647
647
|
}
|
|
648
|
-
|
|
648
|
+
|
|
649
|
+
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
650
|
+
let openAiModel = await settings.get('openAiModel');
|
|
651
|
+
let openAiError = await getOpenAiError(request.app.gt);
|
|
649
652
|
|
|
650
653
|
return h.view(
|
|
651
|
-
'config/
|
|
654
|
+
'config/ai',
|
|
652
655
|
{
|
|
653
|
-
pageTitle: '
|
|
656
|
+
pageTitle: 'AI Processing',
|
|
654
657
|
menuConfig: true,
|
|
655
|
-
|
|
656
|
-
|
|
658
|
+
menuConfigAi: true,
|
|
659
|
+
|
|
660
|
+
errorLog,
|
|
661
|
+
defaultPromptJson: JSON.stringify({ prompt: await getDefaultPrompt() }),
|
|
662
|
+
|
|
663
|
+
values,
|
|
664
|
+
|
|
665
|
+
hasOpenAiAPIKey,
|
|
666
|
+
openAiError,
|
|
667
|
+
openAiModels: await getOpenAiModels(OPEN_AI_MODELS, openAiModel),
|
|
668
|
+
|
|
669
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'),
|
|
670
|
+
examplePayloadsJson: JSON.stringify(
|
|
671
|
+
(await getExampleDocumentsPayloads()).map(entry =>
|
|
672
|
+
Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined })
|
|
673
|
+
)
|
|
674
|
+
)
|
|
657
675
|
},
|
|
658
|
-
{
|
|
676
|
+
{
|
|
677
|
+
layout: 'app'
|
|
678
|
+
}
|
|
659
679
|
);
|
|
660
680
|
}
|
|
661
681
|
});
|
|
662
682
|
|
|
663
683
|
server.route({
|
|
664
684
|
method: 'POST',
|
|
665
|
-
path: '/admin/config/
|
|
685
|
+
path: '/admin/config/ai',
|
|
666
686
|
async handler(request, h) {
|
|
667
687
|
try {
|
|
668
|
-
|
|
669
|
-
|
|
688
|
+
let contentFn;
|
|
689
|
+
try {
|
|
690
|
+
if (request.payload.contentFnJson === '') {
|
|
691
|
+
contentFn = null;
|
|
692
|
+
} else {
|
|
693
|
+
contentFn = JSON.parse(request.payload.contentFnJson);
|
|
694
|
+
if (typeof contentFn !== 'string') {
|
|
695
|
+
throw new Error('Invalid Format');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} catch (err) {
|
|
699
|
+
err.details = {
|
|
700
|
+
contentFnJson: 'Invalid JSON'
|
|
701
|
+
};
|
|
702
|
+
throw err;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
let data = {
|
|
706
|
+
generateEmailSummary: request.payload.generateEmailSummary,
|
|
707
|
+
openAiModel: request.payload.openAiModel,
|
|
708
|
+
openAiAPIUrl: request.payload.openAiAPIUrl,
|
|
709
|
+
openAiPrompt: (request.payload.openAiPrompt || '').toString(),
|
|
710
|
+
openAiPreProcessingFn: contentFn,
|
|
711
|
+
openAiTemperature: request.payload.openAiTemperature,
|
|
712
|
+
openAiTopP: request.payload.openAiTopP,
|
|
713
|
+
openAiMaxTokens: request.payload.openAiMaxTokens
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
let defaultUserPrompt = await getDefaultPrompt();
|
|
717
|
+
|
|
718
|
+
if (!data.openAiPrompt.trim() || data.openAiPrompt.trim() === defaultUserPrompt.trim()) {
|
|
719
|
+
data.openAiPrompt = '';
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (typeof request.payload.openAiAPIKey === 'string') {
|
|
723
|
+
data.openAiAPIKey = request.payload.openAiAPIKey;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (typeof request.payload.openAiAPIUrl === 'string') {
|
|
727
|
+
data.openAiAPIUrl = request.payload.openAiAPIUrl;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
for (let key of Object.keys(data)) {
|
|
731
|
+
await settings.set(key, data[key]);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
735
|
+
|
|
736
|
+
return h.redirect('/admin/config/ai');
|
|
737
|
+
} catch (err) {
|
|
738
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
739
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
740
|
+
|
|
741
|
+
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
742
|
+
let openAiError = await getOpenAiError(request.app.gt);
|
|
743
|
+
|
|
744
|
+
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
745
|
+
if (entry.error && typeof entry.error === 'string') {
|
|
746
|
+
entry.error = entry.error
|
|
747
|
+
.replace(/\r?\n/g, '\n')
|
|
748
|
+
.replace(/^\s+at\s+.*$/gm, '')
|
|
749
|
+
.replace(/\n+/g, '\n')
|
|
750
|
+
.trim()
|
|
751
|
+
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
752
|
+
}
|
|
753
|
+
return entry;
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
return h.view(
|
|
757
|
+
'config/ai',
|
|
758
|
+
{
|
|
759
|
+
pageTitle: 'AI Processing',
|
|
760
|
+
menuConfig: true,
|
|
761
|
+
menuConfigAi: true,
|
|
762
|
+
|
|
763
|
+
errorLog,
|
|
764
|
+
defaultPromptJson: JSON.stringify({ prompt: await getDefaultPrompt() }),
|
|
765
|
+
|
|
766
|
+
hasOpenAiAPIKey,
|
|
767
|
+
openAiError,
|
|
768
|
+
openAiModels: await getOpenAiModels(OPEN_AI_MODELS, request.payload.openAiModel),
|
|
769
|
+
|
|
770
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'),
|
|
771
|
+
examplePayloadsJson: JSON.stringify(
|
|
772
|
+
(await getExampleDocumentsPayloads()).map(entry =>
|
|
773
|
+
Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined })
|
|
774
|
+
)
|
|
775
|
+
)
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
layout: 'app'
|
|
779
|
+
}
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
options: {
|
|
784
|
+
validate: {
|
|
785
|
+
options: {
|
|
786
|
+
stripUnknown: true,
|
|
787
|
+
abortEarly: false,
|
|
788
|
+
convert: true
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
async failAction(request, h, err) {
|
|
792
|
+
let errors = {};
|
|
793
|
+
|
|
794
|
+
if (err.details) {
|
|
795
|
+
err.details.forEach(detail => {
|
|
796
|
+
if (!errors[detail.path]) {
|
|
797
|
+
errors[detail.path] = detail.message;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
803
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
804
|
+
|
|
805
|
+
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
806
|
+
let openAiError = await getOpenAiError(request.app.gt);
|
|
807
|
+
|
|
808
|
+
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
809
|
+
if (entry.error && typeof entry.error === 'string') {
|
|
810
|
+
entry.error = entry.error
|
|
811
|
+
.replace(/\r?\n/g, '\n')
|
|
812
|
+
.replace(/^\s+at\s+.*$/gm, '')
|
|
813
|
+
.replace(/\n+/g, '\n')
|
|
814
|
+
.trim()
|
|
815
|
+
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
816
|
+
}
|
|
817
|
+
return entry;
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
return h
|
|
821
|
+
.view(
|
|
822
|
+
'config/ai',
|
|
823
|
+
{
|
|
824
|
+
pageTitle: 'AI Processing',
|
|
825
|
+
menuConfig: true,
|
|
826
|
+
menuConfigAi: true,
|
|
827
|
+
|
|
828
|
+
errorLog,
|
|
829
|
+
defaultPromptJson: JSON.stringify({ prompt: await getDefaultPrompt() }),
|
|
830
|
+
|
|
831
|
+
hasOpenAiAPIKey,
|
|
832
|
+
openAiError,
|
|
833
|
+
openAiModels: await getOpenAiModels(OPEN_AI_MODELS, request.payload.openAiModel),
|
|
834
|
+
|
|
835
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'),
|
|
836
|
+
examplePayloadsJson: JSON.stringify(
|
|
837
|
+
(await getExampleDocumentsPayloads()).map(entry =>
|
|
838
|
+
Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined })
|
|
839
|
+
)
|
|
840
|
+
),
|
|
841
|
+
|
|
842
|
+
errors
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
layout: 'app'
|
|
846
|
+
}
|
|
847
|
+
)
|
|
848
|
+
.takeover();
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
payload: Joi.object({
|
|
852
|
+
generateEmailSummary: settingsSchema.generateEmailSummary.default(false),
|
|
853
|
+
openAiAPIKey: settingsSchema.openAiAPIKey.empty(''),
|
|
854
|
+
openAiModel: settingsSchema.openAiModel.empty(''),
|
|
855
|
+
|
|
856
|
+
openAiAPIUrl: settingsSchema.openAiAPIUrl.default(''),
|
|
857
|
+
|
|
858
|
+
openAiPrompt: settingsSchema.openAiPrompt.default(''),
|
|
859
|
+
|
|
860
|
+
contentFnJson: Joi.string()
|
|
861
|
+
.max(1024 * 1024)
|
|
862
|
+
.default('')
|
|
863
|
+
.allow('')
|
|
864
|
+
.trim()
|
|
865
|
+
.description('Filter function'),
|
|
866
|
+
|
|
867
|
+
openAiTemperature: settingsSchema.openAiTemperature.default(''),
|
|
868
|
+
openAiTopP: settingsSchema.openAiTopP.default(''),
|
|
869
|
+
openAiMaxTokens: settingsSchema.openAiMaxTokens.default('')
|
|
870
|
+
})
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
server.route({
|
|
876
|
+
method: 'POST',
|
|
877
|
+
path: '/admin/config/ai/test-prompt',
|
|
878
|
+
async handler(request) {
|
|
879
|
+
try {
|
|
880
|
+
request.logger.info({ msg: 'Prompt test' });
|
|
881
|
+
|
|
882
|
+
const parsed = await simpleParser(Buffer.from(request.payload.emailFile, 'base64'));
|
|
883
|
+
|
|
884
|
+
const response = {};
|
|
885
|
+
response.summary = await call({
|
|
886
|
+
cmd: 'generateSummary',
|
|
887
|
+
data: {
|
|
888
|
+
message: {
|
|
889
|
+
headers: parsed.headerLines.map(header => libmime.decodeHeader(header.line)),
|
|
890
|
+
attachments: parsed.attachments,
|
|
891
|
+
html: parsed.html,
|
|
892
|
+
text: parsed.text
|
|
893
|
+
},
|
|
894
|
+
openAiAPIKey: request.payload.openAiAPIKey,
|
|
895
|
+
openAiModel: request.payload.openAiModel,
|
|
896
|
+
openAiAPIUrl: request.payload.openAiAPIUrl,
|
|
897
|
+
openAiPrompt: request.payload.openAiPrompt,
|
|
898
|
+
openAiTemperature: request.payload.openAiTemperature,
|
|
899
|
+
openAiTopP: request.payload.openAiTopP,
|
|
900
|
+
openAiMaxTokens: request.payload.openAiMaxTokens
|
|
901
|
+
},
|
|
902
|
+
timeout: 2 * 60 * 1000
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// crux from olden times
|
|
906
|
+
for (let key of Object.keys(response.summary)) {
|
|
907
|
+
// remove meta keys from output
|
|
908
|
+
if (key.charAt(0) === '_' || response.summary[key] === '') {
|
|
909
|
+
delete response.summary[key];
|
|
910
|
+
}
|
|
911
|
+
if (key === 'riskAssessment') {
|
|
912
|
+
response.riskAssessment = response.summary.riskAssessment;
|
|
913
|
+
delete response.summary.riskAssessment;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return { success: true, response };
|
|
918
|
+
} catch (err) {
|
|
919
|
+
request.logger.error({ msg: 'Failed to test prompt', err });
|
|
920
|
+
return { success: false, error: err.message };
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
options: {
|
|
924
|
+
payload: {
|
|
925
|
+
maxBytes: MAX_BODY_SIZE,
|
|
926
|
+
timeout: MAX_PAYLOAD_TIMEOUT
|
|
927
|
+
},
|
|
928
|
+
validate: {
|
|
929
|
+
options: {
|
|
930
|
+
stripUnknown: true,
|
|
931
|
+
abortEarly: false,
|
|
932
|
+
convert: true
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
failAction,
|
|
936
|
+
|
|
937
|
+
payload: Joi.object({
|
|
938
|
+
emailFile: Joi.string().base64({ paddingRequired: false }).required(),
|
|
939
|
+
openAiAPIKey: settingsSchema.openAiAPIKey.empty(''),
|
|
940
|
+
openAiModel: settingsSchema.openAiModel.empty(''),
|
|
941
|
+
openAiAPIUrl: settingsSchema.openAiAPIUrl.default(''),
|
|
942
|
+
openAiPrompt: settingsSchema.openAiPrompt.default(''),
|
|
943
|
+
openAiTemperature: settingsSchema.openAiTemperature.empty(''),
|
|
944
|
+
openAiTopP: settingsSchema.openAiTopP.empty(''),
|
|
945
|
+
openAiMaxTokens: settingsSchema.openAiMaxTokens.empty('')
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
server.route({
|
|
952
|
+
method: 'POST',
|
|
953
|
+
path: '/admin/config/ai/reload-models',
|
|
954
|
+
async handler(request) {
|
|
955
|
+
try {
|
|
956
|
+
request.logger.info({ msg: 'Reload models' });
|
|
957
|
+
|
|
958
|
+
const { models } = await call({
|
|
959
|
+
cmd: 'openAiListModels',
|
|
960
|
+
data: {
|
|
961
|
+
openAiAPIKey: request.payload.openAiAPIKey,
|
|
962
|
+
openAiAPIUrl: request.payload.openAiAPIUrl
|
|
963
|
+
},
|
|
964
|
+
timeout: 2 * 60 * 1000
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
if (models && models.length) {
|
|
968
|
+
await settings.set('openAiModels', models);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return { success: true, models };
|
|
972
|
+
} catch (err) {
|
|
973
|
+
request.logger.error({ msg: 'Failed reloading OpenAI models', err });
|
|
974
|
+
return {
|
|
975
|
+
success: false,
|
|
976
|
+
error: err.message
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
options: {
|
|
981
|
+
validate: {
|
|
982
|
+
options: {
|
|
983
|
+
stripUnknown: true,
|
|
984
|
+
abortEarly: false,
|
|
985
|
+
convert: true
|
|
986
|
+
},
|
|
987
|
+
|
|
988
|
+
failAction,
|
|
989
|
+
|
|
990
|
+
payload: Joi.object({
|
|
991
|
+
openAiAPIKey: settingsSchema.openAiAPIKey.empty(''),
|
|
992
|
+
openAiAPIUrl: settingsSchema.openAiAPIUrl.default('')
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
server.route({
|
|
999
|
+
method: 'POST',
|
|
1000
|
+
path: '/admin/config/service/preview',
|
|
1001
|
+
async handler(request, h) {
|
|
1002
|
+
return h.view(
|
|
1003
|
+
'config/service-preview',
|
|
1004
|
+
{
|
|
1005
|
+
pageBrandName: request.payload.pageBrandName,
|
|
1006
|
+
embeddedTemplateHeader: request.payload.templateHeader,
|
|
1007
|
+
embeddedTemplateHtmlHead: request.payload.templateHtmlHead
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
layout: 'public'
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
},
|
|
1014
|
+
options: {
|
|
1015
|
+
validate: {
|
|
1016
|
+
options: {
|
|
1017
|
+
stripUnknown: true,
|
|
1018
|
+
abortEarly: false,
|
|
1019
|
+
convert: true
|
|
1020
|
+
},
|
|
1021
|
+
|
|
1022
|
+
async failAction(request, h, err) {
|
|
1023
|
+
request.logger.error({ msg: 'Failed to process preview', err });
|
|
1024
|
+
return h.redirect('/admin').takeover();
|
|
1025
|
+
},
|
|
1026
|
+
|
|
1027
|
+
payload: Joi.object({
|
|
1028
|
+
pageBrandName: settingsSchema.pageBrandName.default(''),
|
|
1029
|
+
templateHeader: settingsSchema.templateHeader.default(''),
|
|
1030
|
+
templateHtmlHead: settingsSchema.templateHtmlHead.default('')
|
|
1031
|
+
})
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
server.route({
|
|
1037
|
+
method: 'POST',
|
|
1038
|
+
path: '/admin/config/clear-error',
|
|
1039
|
+
async handler(request) {
|
|
1040
|
+
switch (request.payload.alert) {
|
|
1041
|
+
case 'open-ai':
|
|
1042
|
+
await redis.del(`${REDIS_PREFIX}:openai:error`);
|
|
1043
|
+
break;
|
|
1044
|
+
|
|
1045
|
+
case 'webhook-default':
|
|
1046
|
+
await settings.clear('webhookErrorFlag');
|
|
1047
|
+
break;
|
|
1048
|
+
|
|
1049
|
+
case 'webhook-route':
|
|
1050
|
+
if (request.payload.entry) {
|
|
1051
|
+
await redis.hdel(`${REDIS_PREFIX}wh:c`, `${request.payload.entry}:webhookErrorFlag`);
|
|
1052
|
+
}
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return { success: true };
|
|
1057
|
+
},
|
|
1058
|
+
options: {
|
|
1059
|
+
validate: {
|
|
1060
|
+
options: {
|
|
1061
|
+
stripUnknown: true,
|
|
1062
|
+
abortEarly: false,
|
|
1063
|
+
convert: true
|
|
1064
|
+
},
|
|
1065
|
+
|
|
1066
|
+
failAction,
|
|
1067
|
+
|
|
1068
|
+
payload: Joi.object({
|
|
1069
|
+
alert: Joi.string().required().max(1024),
|
|
1070
|
+
entry: Joi.string().empty('').max(1024).trim()
|
|
1071
|
+
})
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
server.route({
|
|
1077
|
+
method: 'POST',
|
|
1078
|
+
path: '/admin/config/service/clean',
|
|
1079
|
+
async handler(request) {
|
|
1080
|
+
let errors = [];
|
|
1081
|
+
|
|
1082
|
+
for (let queue of [submitQueue, notifyQueue, documentsQueue]) {
|
|
1083
|
+
for (let type of ['failed', 'completed']) {
|
|
1084
|
+
try {
|
|
1085
|
+
await queue.clean(1000, 100000, type);
|
|
1086
|
+
request.logger.trace({ msg: 'Queue cleaned', queue: queue.name, type });
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
request.logger.error({ msg: 'Failed to clean queue', queue: queue.name, type, err });
|
|
1089
|
+
errors.push(err.message);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (errors.length) {
|
|
1095
|
+
return { success: false, error: 'Cleaning failed for some queues' };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
return { success: true };
|
|
1099
|
+
},
|
|
1100
|
+
options: {
|
|
1101
|
+
validate: {
|
|
1102
|
+
options: {
|
|
1103
|
+
stripUnknown: true,
|
|
1104
|
+
abortEarly: false,
|
|
1105
|
+
convert: true
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
server.route({
|
|
1112
|
+
method: 'GET',
|
|
1113
|
+
path: '/admin/config/logging',
|
|
1114
|
+
async handler(request, h) {
|
|
1115
|
+
let values = (await settings.get('logs')) || {};
|
|
1116
|
+
|
|
1117
|
+
if (typeof values.maxLogLines === 'undefined') {
|
|
1118
|
+
values.maxLogLines = DEFAULT_MAX_LOG_LINES;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
values.accounts = [].concat(values.accounts || []).join('\n');
|
|
1122
|
+
|
|
1123
|
+
let sentryValues = await settings.getMulti('sentryEnabled', 'sentryDsn');
|
|
1124
|
+
values.sentryEnabled = !!sentryValues.sentryEnabled;
|
|
1125
|
+
values.sentryDsn = sentryValues.sentryDsn || '';
|
|
1126
|
+
|
|
1127
|
+
return h.view(
|
|
1128
|
+
'config/logging',
|
|
1129
|
+
{
|
|
1130
|
+
pageTitle: 'Logging',
|
|
1131
|
+
menuConfig: true,
|
|
1132
|
+
menuConfigLogging: true,
|
|
1133
|
+
|
|
1134
|
+
sentryEnvManaged: !!readEnvValue('SENTRY_DSN'),
|
|
1135
|
+
|
|
1136
|
+
values
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
layout: 'app'
|
|
1140
|
+
}
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
server.route({
|
|
1146
|
+
method: 'POST',
|
|
1147
|
+
path: '/admin/config/logging',
|
|
1148
|
+
async handler(request, h) {
|
|
1149
|
+
try {
|
|
1150
|
+
const data = {
|
|
1151
|
+
logs: {
|
|
670
1152
|
all: !!request.payload.all,
|
|
671
1153
|
maxLogLines: request.payload.maxLogLines || 0
|
|
672
1154
|
}
|
|
673
1155
|
};
|
|
674
1156
|
|
|
1157
|
+
if (!readEnvValue('SENTRY_DSN')) {
|
|
1158
|
+
// the form renders these fields as disabled when the DSN is pinned by the
|
|
1159
|
+
// environment, so do not overwrite the stored values in that case
|
|
1160
|
+
data.sentryEnabled = !!request.payload.sentryEnabled;
|
|
1161
|
+
data.sentryDsn = request.payload.sentryDsn || '';
|
|
1162
|
+
}
|
|
1163
|
+
|
|
675
1164
|
for (let key of Object.keys(data)) {
|
|
676
1165
|
await settings.set(key, data[key]);
|
|
677
1166
|
}
|
|
678
1167
|
|
|
679
1168
|
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
1169
|
+
|
|
680
1170
|
return h.redirect('/admin/config/logging');
|
|
681
1171
|
} catch (err) {
|
|
682
1172
|
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
@@ -689,15 +1179,23 @@ function init(args) {
|
|
|
689
1179
|
menuConfig: true,
|
|
690
1180
|
menuConfigWebhooks: true
|
|
691
1181
|
},
|
|
692
|
-
{
|
|
1182
|
+
{
|
|
1183
|
+
layout: 'app'
|
|
1184
|
+
}
|
|
693
1185
|
);
|
|
694
1186
|
}
|
|
695
1187
|
},
|
|
696
1188
|
options: {
|
|
697
1189
|
validate: {
|
|
698
|
-
options: {
|
|
1190
|
+
options: {
|
|
1191
|
+
stripUnknown: true,
|
|
1192
|
+
abortEarly: false,
|
|
1193
|
+
convert: true
|
|
1194
|
+
},
|
|
1195
|
+
|
|
699
1196
|
async failAction(request, h, err) {
|
|
700
1197
|
let errors = {};
|
|
1198
|
+
|
|
701
1199
|
if (err.details) {
|
|
702
1200
|
err.details.forEach(detail => {
|
|
703
1201
|
if (!errors[detail.path]) {
|
|
@@ -716,18 +1214,21 @@ function init(args) {
|
|
|
716
1214
|
pageTitle: 'Logging',
|
|
717
1215
|
menuConfig: true,
|
|
718
1216
|
menuConfigWebhooks: true,
|
|
1217
|
+
|
|
719
1218
|
errors
|
|
720
1219
|
},
|
|
721
|
-
{
|
|
1220
|
+
{
|
|
1221
|
+
layout: 'app'
|
|
1222
|
+
}
|
|
722
1223
|
)
|
|
723
1224
|
.takeover();
|
|
724
1225
|
},
|
|
1226
|
+
|
|
725
1227
|
payload: Joi.object(configLoggingSchema)
|
|
726
1228
|
}
|
|
727
1229
|
}
|
|
728
1230
|
});
|
|
729
1231
|
|
|
730
|
-
// Logging reconnect route
|
|
731
1232
|
server.route({
|
|
732
1233
|
method: 'POST',
|
|
733
1234
|
path: '/admin/config/logging/reconnect',
|
|
@@ -744,7 +1245,10 @@ function init(args) {
|
|
|
744
1245
|
}
|
|
745
1246
|
}
|
|
746
1247
|
|
|
747
|
-
return {
|
|
1248
|
+
return {
|
|
1249
|
+
success: true,
|
|
1250
|
+
accounts: requested
|
|
1251
|
+
};
|
|
748
1252
|
} catch (err) {
|
|
749
1253
|
request.logger.error({ msg: 'Failed to request reconnect', err, accounts: request.payload.accounts });
|
|
750
1254
|
return { success: false, error: err.message };
|
|
@@ -752,16 +1256,26 @@ function init(args) {
|
|
|
752
1256
|
},
|
|
753
1257
|
options: {
|
|
754
1258
|
validate: {
|
|
755
|
-
options: {
|
|
1259
|
+
options: {
|
|
1260
|
+
stripUnknown: true,
|
|
1261
|
+
abortEarly: false,
|
|
1262
|
+
convert: true
|
|
1263
|
+
},
|
|
1264
|
+
|
|
756
1265
|
failAction,
|
|
1266
|
+
|
|
757
1267
|
payload: Joi.object({
|
|
758
|
-
accounts: Joi.array()
|
|
1268
|
+
accounts: Joi.array()
|
|
1269
|
+
.items(Joi.string().max(256))
|
|
1270
|
+
.default([])
|
|
1271
|
+
.example(['account-id-1', 'account-id-2'])
|
|
1272
|
+
.description('Request reconnect for listed accounts')
|
|
1273
|
+
.label('LoggedAccounts')
|
|
759
1274
|
})
|
|
760
1275
|
}
|
|
761
1276
|
}
|
|
762
1277
|
});
|
|
763
1278
|
|
|
764
|
-
// Webhooks test route
|
|
765
1279
|
server.route({
|
|
766
1280
|
method: 'POST',
|
|
767
1281
|
path: '/admin/config/webhooks/test',
|
|
@@ -773,7 +1287,11 @@ function init(args) {
|
|
|
773
1287
|
|
|
774
1288
|
const webhooks = request.payload.webhooks;
|
|
775
1289
|
if (!webhooks) {
|
|
776
|
-
return {
|
|
1290
|
+
return {
|
|
1291
|
+
success: false,
|
|
1292
|
+
target: webhooks,
|
|
1293
|
+
error: 'Webhook URL is not set'
|
|
1294
|
+
};
|
|
777
1295
|
}
|
|
778
1296
|
|
|
779
1297
|
let parsed = new URL(webhooks);
|
|
@@ -800,9 +1318,15 @@ function init(args) {
|
|
|
800
1318
|
.map(line => {
|
|
801
1319
|
let sep = line.indexOf(':');
|
|
802
1320
|
if (sep >= 0) {
|
|
803
|
-
return {
|
|
1321
|
+
return {
|
|
1322
|
+
key: line.substring(0, sep).trim(),
|
|
1323
|
+
value: line.substring(sep + 1).trim()
|
|
1324
|
+
};
|
|
804
1325
|
}
|
|
805
|
-
return {
|
|
1326
|
+
return {
|
|
1327
|
+
key: line,
|
|
1328
|
+
value: ''
|
|
1329
|
+
};
|
|
806
1330
|
});
|
|
807
1331
|
|
|
808
1332
|
customHeaders.forEach(header => {
|
|
@@ -813,6 +1337,7 @@ function init(args) {
|
|
|
813
1337
|
let duration;
|
|
814
1338
|
try {
|
|
815
1339
|
let res;
|
|
1340
|
+
|
|
816
1341
|
let serviceUrl = await settings.get('serviceUrl');
|
|
817
1342
|
|
|
818
1343
|
try {
|
|
@@ -825,7 +1350,9 @@ function init(args) {
|
|
|
825
1350
|
account: null,
|
|
826
1351
|
date: new Date().toISOString(),
|
|
827
1352
|
event: 'test',
|
|
828
|
-
data: {
|
|
1353
|
+
data: {
|
|
1354
|
+
nonce: crypto.randomBytes(NONCE_BYTES).toString('base64url')
|
|
1355
|
+
}
|
|
829
1356
|
}),
|
|
830
1357
|
headers,
|
|
831
1358
|
dispatcher: httpAgent.retry
|
|
@@ -842,194 +1369,151 @@ function init(args) {
|
|
|
842
1369
|
throw err;
|
|
843
1370
|
}
|
|
844
1371
|
|
|
845
|
-
return {
|
|
1372
|
+
return {
|
|
1373
|
+
success: true,
|
|
1374
|
+
target: webhooks,
|
|
1375
|
+
duration
|
|
1376
|
+
};
|
|
846
1377
|
} catch (err) {
|
|
847
1378
|
request.logger.error({ msg: 'Failed posting webhook', webhooks, event: 'test', err });
|
|
848
|
-
return {
|
|
1379
|
+
return {
|
|
1380
|
+
success: false,
|
|
1381
|
+
target: webhooks,
|
|
1382
|
+
duration,
|
|
1383
|
+
error: err.message,
|
|
1384
|
+
code: err.code
|
|
1385
|
+
};
|
|
849
1386
|
}
|
|
850
1387
|
},
|
|
851
1388
|
options: {
|
|
852
1389
|
tags: ['test'],
|
|
853
1390
|
validate: {
|
|
854
|
-
options: {
|
|
1391
|
+
options: {
|
|
1392
|
+
stripUnknown: true,
|
|
1393
|
+
abortEarly: false,
|
|
1394
|
+
convert: true
|
|
1395
|
+
},
|
|
1396
|
+
|
|
855
1397
|
failAction,
|
|
1398
|
+
|
|
856
1399
|
payload: Joi.object({
|
|
857
1400
|
webhooks: Joi.string()
|
|
858
|
-
.uri({
|
|
859
|
-
|
|
1401
|
+
.uri({
|
|
1402
|
+
scheme: ['http', 'https'],
|
|
1403
|
+
allowRelative: false
|
|
1404
|
+
})
|
|
1405
|
+
.allow('')
|
|
1406
|
+
.example('https://myservice.com/imap/webhooks')
|
|
1407
|
+
.description('Webhook URL'),
|
|
860
1408
|
customHeaders: Joi.string()
|
|
861
1409
|
.allow('')
|
|
862
1410
|
.trim()
|
|
863
1411
|
.max(10 * 1024)
|
|
864
|
-
.default('')
|
|
1412
|
+
.default('')
|
|
1413
|
+
.description('Custom request headers'),
|
|
865
1414
|
payload: Joi.string()
|
|
866
1415
|
.max(1024 * 1024)
|
|
867
1416
|
.empty('')
|
|
868
1417
|
.trim()
|
|
1418
|
+
.description('Example JSON payload')
|
|
869
1419
|
})
|
|
870
1420
|
}
|
|
871
1421
|
}
|
|
872
1422
|
});
|
|
873
1423
|
|
|
874
|
-
//
|
|
875
|
-
server.route({
|
|
876
|
-
method: 'GET',
|
|
877
|
-
path: '/admin/config/ai',
|
|
878
|
-
async handler(request, h) {
|
|
879
|
-
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
880
|
-
if (entry.error && typeof entry.error === 'string') {
|
|
881
|
-
entry.error = entry.error
|
|
882
|
-
.replace(/\r?\n/g, '\n')
|
|
883
|
-
.replace(/^\s+at\s+.*$/gm, '')
|
|
884
|
-
.replace(/\n+/g, '\n')
|
|
885
|
-
.trim()
|
|
886
|
-
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
887
|
-
}
|
|
888
|
-
return entry;
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
const values = {
|
|
892
|
-
generateEmailSummary: (await settings.get('generateEmailSummary')) || false,
|
|
893
|
-
openAiPrompt: ((await settings.get('openAiPrompt')) || '').toString(),
|
|
894
|
-
contentFnJson: JSON.stringify(
|
|
895
|
-
((await settings.get(`openAiPreProcessingFn`)) || '').toString() ||
|
|
896
|
-
`// Pass all emails
|
|
897
|
-
return true;`
|
|
898
|
-
),
|
|
899
|
-
openAiAPIUrl: ((await settings.get('openAiAPIUrl')) || '').toString(),
|
|
900
|
-
openAiTemperature: ((await settings.get('openAiTemperature')) || '').toString(),
|
|
901
|
-
openAiTopP: ((await settings.get('openAiTopP')) || '').toString(),
|
|
902
|
-
openAiMaxTokens: ((await settings.get('openAiMaxTokens')) || '').toString()
|
|
903
|
-
};
|
|
904
|
-
|
|
905
|
-
if (!values.openAiPrompt.trim()) {
|
|
906
|
-
values.openAiPrompt = await getDefaultPrompt();
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
910
|
-
let openAiModel = await settings.get('openAiModel');
|
|
911
|
-
let openAiError = await getOpenAiError(request.app.gt);
|
|
1424
|
+
// Webhook, template, gateway, and token routes are in admin-entities-routes.js
|
|
912
1425
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1426
|
+
server.route({
|
|
1427
|
+
method: 'GET',
|
|
1428
|
+
path: '/admin/config/license',
|
|
1429
|
+
async handler(request, h) {
|
|
1430
|
+
await call({ cmd: 'checkLicense' });
|
|
1431
|
+
|
|
1432
|
+
let subexp = await settings.get('subexp');
|
|
1433
|
+
let expiresDays;
|
|
1434
|
+
if (subexp && !(request.app.licenseInfo && request.app.licenseInfo.details && request.app.licenseInfo.details.lt)) {
|
|
1435
|
+
let delayMs = new Date(subexp) - Date.now();
|
|
1436
|
+
expiresDays = Math.max(Math.ceil(delayMs / (24 * 3600 * 1000)), 0);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return h.view(
|
|
1440
|
+
'config/license',
|
|
1441
|
+
{
|
|
1442
|
+
pageTitle: 'License',
|
|
1443
|
+
menuLicense: true,
|
|
1444
|
+
hideLicenseWarning: true,
|
|
1445
|
+
menuConfig: true,
|
|
1446
|
+
menuConfigLicense: true,
|
|
1447
|
+
|
|
1448
|
+
subexp,
|
|
1449
|
+
expiresDays,
|
|
1450
|
+
|
|
1451
|
+
showLicenseText:
|
|
1452
|
+
!request.app.licenseInfo ||
|
|
1453
|
+
!request.app.licenseInfo.active ||
|
|
1454
|
+
(request.app.licenseInfo.details && request.app.licenseInfo.details.trial)
|
|
931
1455
|
},
|
|
932
|
-
{
|
|
1456
|
+
{
|
|
1457
|
+
layout: 'app'
|
|
1458
|
+
}
|
|
933
1459
|
);
|
|
934
1460
|
}
|
|
935
1461
|
});
|
|
936
1462
|
|
|
937
1463
|
server.route({
|
|
938
1464
|
method: 'POST',
|
|
939
|
-
path: '/admin/config/
|
|
1465
|
+
path: '/admin/config/license',
|
|
940
1466
|
async handler(request, h) {
|
|
941
1467
|
try {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if (typeof contentFn !== 'string') {
|
|
949
|
-
throw new Error('Invalid Format');
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
} catch (err) {
|
|
953
|
-
err.details = { contentFnJson: 'Invalid JSON' };
|
|
1468
|
+
// update license
|
|
1469
|
+
const licenseInfo = await call({ cmd: 'updateLicense', license: request.payload.license });
|
|
1470
|
+
if (!licenseInfo) {
|
|
1471
|
+
let err = new Error('Failed to update license. Check license file contents.');
|
|
1472
|
+
err.statusCode = 403;
|
|
1473
|
+
err.details = { license: err.message };
|
|
954
1474
|
throw err;
|
|
955
1475
|
}
|
|
956
1476
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
openAiModel: request.payload.openAiModel,
|
|
960
|
-
openAiAPIUrl: request.payload.openAiAPIUrl,
|
|
961
|
-
openAiPrompt: (request.payload.openAiPrompt || '').toString(),
|
|
962
|
-
openAiPreProcessingFn: contentFn,
|
|
963
|
-
openAiTemperature: request.payload.openAiTemperature,
|
|
964
|
-
openAiTopP: request.payload.openAiTopP,
|
|
965
|
-
openAiMaxTokens: request.payload.openAiMaxTokens
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
let defaultUserPrompt = await getDefaultPrompt();
|
|
969
|
-
if (!data.openAiPrompt.trim() || data.openAiPrompt.trim() === defaultUserPrompt.trim()) {
|
|
970
|
-
data.openAiPrompt = '';
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (typeof request.payload.openAiAPIKey === 'string') {
|
|
974
|
-
data.openAiAPIKey = request.payload.openAiAPIKey;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
if (typeof request.payload.openAiAPIUrl === 'string') {
|
|
978
|
-
data.openAiAPIUrl = request.payload.openAiAPIUrl;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
for (let key of Object.keys(data)) {
|
|
982
|
-
await settings.set(key, data[key]);
|
|
1477
|
+
if (licenseInfo.active) {
|
|
1478
|
+
await request.flash({ type: 'info', message: `License activated` });
|
|
983
1479
|
}
|
|
984
1480
|
|
|
985
|
-
|
|
986
|
-
return h.redirect('/admin/config/ai');
|
|
1481
|
+
return h.redirect('/admin/config/license');
|
|
987
1482
|
} catch (err) {
|
|
988
|
-
await request.flash({ type: 'danger', message: `Couldn't
|
|
989
|
-
request.logger.error({ msg: 'Failed to
|
|
990
|
-
|
|
991
|
-
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
992
|
-
let openAiError = await getOpenAiError(request.app.gt);
|
|
993
|
-
|
|
994
|
-
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
995
|
-
if (entry.error && typeof entry.error === 'string') {
|
|
996
|
-
entry.error = entry.error
|
|
997
|
-
.replace(/\r?\n/g, '\n')
|
|
998
|
-
.replace(/^\s+at\s+.*$/gm, '')
|
|
999
|
-
.replace(/\n+/g, '\n')
|
|
1000
|
-
.trim()
|
|
1001
|
-
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
1002
|
-
}
|
|
1003
|
-
return entry;
|
|
1004
|
-
});
|
|
1483
|
+
await request.flash({ type: 'danger', message: `Couldn't register license. Check the key and try again.` });
|
|
1484
|
+
request.logger.error({ msg: 'Failed to register license key', err });
|
|
1005
1485
|
|
|
1006
1486
|
return h.view(
|
|
1007
|
-
'config/
|
|
1487
|
+
'config/license',
|
|
1008
1488
|
{
|
|
1009
|
-
pageTitle: '
|
|
1489
|
+
pageTitle: 'License',
|
|
1010
1490
|
menuConfig: true,
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
(await getExampleDocumentsPayloads()).map(entry =>
|
|
1020
|
-
Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined })
|
|
1021
|
-
)
|
|
1022
|
-
)
|
|
1491
|
+
menuConfigWebhooks: true,
|
|
1492
|
+
|
|
1493
|
+
errors: err.details,
|
|
1494
|
+
showLicenseText:
|
|
1495
|
+
(err.details && !!err.details.license) ||
|
|
1496
|
+
!request.app.licenseInfo ||
|
|
1497
|
+
!request.app.licenseInfo.active ||
|
|
1498
|
+
(request.app.licenseInfo.details && request.app.licenseInfo.details.trial)
|
|
1023
1499
|
},
|
|
1024
|
-
{
|
|
1500
|
+
{
|
|
1501
|
+
layout: 'app'
|
|
1502
|
+
}
|
|
1025
1503
|
);
|
|
1026
1504
|
}
|
|
1027
1505
|
},
|
|
1028
1506
|
options: {
|
|
1029
1507
|
validate: {
|
|
1030
|
-
options: {
|
|
1508
|
+
options: {
|
|
1509
|
+
stripUnknown: true,
|
|
1510
|
+
abortEarly: false,
|
|
1511
|
+
convert: true
|
|
1512
|
+
},
|
|
1513
|
+
|
|
1031
1514
|
async failAction(request, h, err) {
|
|
1032
1515
|
let errors = {};
|
|
1516
|
+
|
|
1033
1517
|
if (err.details) {
|
|
1034
1518
|
err.details.forEach(detail => {
|
|
1035
1519
|
if (!errors[detail.path]) {
|
|
@@ -1038,200 +1522,155 @@ return true;`
|
|
|
1038
1522
|
});
|
|
1039
1523
|
}
|
|
1040
1524
|
|
|
1041
|
-
await request.flash({ type: 'danger', message: `Couldn't
|
|
1042
|
-
request.logger.error({ msg: 'Failed to
|
|
1043
|
-
|
|
1044
|
-
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
1045
|
-
let openAiError = await getOpenAiError(request.app.gt);
|
|
1046
|
-
|
|
1047
|
-
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
1048
|
-
if (entry.error && typeof entry.error === 'string') {
|
|
1049
|
-
entry.error = entry.error
|
|
1050
|
-
.replace(/\r?\n/g, '\n')
|
|
1051
|
-
.replace(/^\s+at\s+.*$/gm, '')
|
|
1052
|
-
.replace(/\n+/g, '\n')
|
|
1053
|
-
.trim()
|
|
1054
|
-
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
1055
|
-
}
|
|
1056
|
-
return entry;
|
|
1057
|
-
});
|
|
1525
|
+
await request.flash({ type: 'danger', message: `Couldn't register license. Check the key and try again.` });
|
|
1526
|
+
request.logger.error({ msg: 'Failed to register license key', err });
|
|
1058
1527
|
|
|
1059
1528
|
return h
|
|
1060
1529
|
.view(
|
|
1061
|
-
'config/
|
|
1530
|
+
'config/license',
|
|
1062
1531
|
{
|
|
1063
|
-
pageTitle: '
|
|
1532
|
+
pageTitle: 'License',
|
|
1064
1533
|
menuConfig: true,
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
(
|
|
1074
|
-
Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined })
|
|
1075
|
-
)
|
|
1076
|
-
),
|
|
1077
|
-
errors
|
|
1534
|
+
menuConfigWebhooks: true,
|
|
1535
|
+
|
|
1536
|
+
errors,
|
|
1537
|
+
|
|
1538
|
+
showLicenseText:
|
|
1539
|
+
(errors && !!errors.license) ||
|
|
1540
|
+
!request.app.licenseInfo ||
|
|
1541
|
+
!request.app.licenseInfo.active ||
|
|
1542
|
+
(request.app.licenseInfo.details && request.app.licenseInfo.details.trial)
|
|
1078
1543
|
},
|
|
1079
|
-
{
|
|
1544
|
+
{
|
|
1545
|
+
layout: 'app'
|
|
1546
|
+
}
|
|
1080
1547
|
)
|
|
1081
1548
|
.takeover();
|
|
1082
1549
|
},
|
|
1550
|
+
|
|
1083
1551
|
payload: Joi.object({
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
.max(1024 * 1024)
|
|
1091
|
-
.default('')
|
|
1092
|
-
.allow('')
|
|
1093
|
-
.trim(),
|
|
1094
|
-
openAiTemperature: settingsSchema.openAiTemperature.default(''),
|
|
1095
|
-
openAiTopP: settingsSchema.openAiTopP.default(''),
|
|
1096
|
-
openAiMaxTokens: settingsSchema.openAiMaxTokens.default('')
|
|
1097
|
-
})
|
|
1552
|
+
license: Joi.string()
|
|
1553
|
+
.max(10 * 1024)
|
|
1554
|
+
.required()
|
|
1555
|
+
.example('-----BEGIN LICENSE-----\r\n...')
|
|
1556
|
+
.description('License file')
|
|
1557
|
+
}).label('RegisterLicense')
|
|
1098
1558
|
}
|
|
1099
1559
|
}
|
|
1100
1560
|
});
|
|
1101
1561
|
|
|
1102
|
-
// AI test prompt route
|
|
1103
1562
|
server.route({
|
|
1104
1563
|
method: 'POST',
|
|
1105
|
-
path: '/admin/config/
|
|
1106
|
-
async handler(request) {
|
|
1564
|
+
path: '/admin/config/license/delete',
|
|
1565
|
+
async handler(request, h) {
|
|
1107
1566
|
try {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
data: {
|
|
1116
|
-
message: {
|
|
1117
|
-
headers: parsed.headerLines.map(header => libmime.decodeHeader(header.line)),
|
|
1118
|
-
attachments: parsed.attachments,
|
|
1119
|
-
html: parsed.html,
|
|
1120
|
-
text: parsed.text
|
|
1121
|
-
},
|
|
1122
|
-
openAiAPIKey: request.payload.openAiAPIKey,
|
|
1123
|
-
openAiModel: request.payload.openAiModel,
|
|
1124
|
-
openAiAPIUrl: request.payload.openAiAPIUrl,
|
|
1125
|
-
openAiPrompt: request.payload.openAiPrompt,
|
|
1126
|
-
openAiTemperature: request.payload.openAiTemperature,
|
|
1127
|
-
openAiTopP: request.payload.openAiTopP,
|
|
1128
|
-
openAiMaxTokens: request.payload.openAiMaxTokens
|
|
1129
|
-
},
|
|
1130
|
-
timeout: 2 * 60 * 1000
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
for (let key of Object.keys(response.summary)) {
|
|
1134
|
-
if (key.charAt(0) === '_' || response.summary[key] === '') {
|
|
1135
|
-
delete response.summary[key];
|
|
1136
|
-
}
|
|
1137
|
-
if (key === 'riskAssessment') {
|
|
1138
|
-
response.riskAssessment = response.summary.riskAssessment;
|
|
1139
|
-
delete response.summary.riskAssessment;
|
|
1140
|
-
}
|
|
1567
|
+
const licenseInfo = await call({ cmd: 'removeLicense' });
|
|
1568
|
+
if (!licenseInfo) {
|
|
1569
|
+
let err = new Error('Failed to clear license info');
|
|
1570
|
+
err.statusCode = 403;
|
|
1571
|
+
throw err;
|
|
1572
|
+
} else {
|
|
1573
|
+
await request.flash({ type: 'info', message: `License removed` });
|
|
1141
1574
|
}
|
|
1142
1575
|
|
|
1143
|
-
return
|
|
1576
|
+
return h.redirect('/admin/config/license');
|
|
1144
1577
|
} catch (err) {
|
|
1145
|
-
request.
|
|
1146
|
-
|
|
1578
|
+
await request.flash({ type: 'danger', message: `Couldn't remove license. Try again.` });
|
|
1579
|
+
request.logger.error({ msg: 'Failed to unregister license key', err, token: request.payload.token, remoteAddress: request.app.ip });
|
|
1580
|
+
return h.redirect('/admin/config/license');
|
|
1147
1581
|
}
|
|
1148
1582
|
},
|
|
1149
1583
|
options: {
|
|
1150
|
-
payload: { maxBytes: MAX_BODY_SIZE, timeout: MAX_PAYLOAD_TIMEOUT },
|
|
1151
1584
|
validate: {
|
|
1152
|
-
options: {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}
|
|
1585
|
+
options: {
|
|
1586
|
+
stripUnknown: true,
|
|
1587
|
+
abortEarly: false,
|
|
1588
|
+
convert: true
|
|
1589
|
+
},
|
|
1590
|
+
|
|
1591
|
+
async failAction(request, h, err) {
|
|
1592
|
+
await request.flash({ type: 'danger', message: `Couldn't remove license. Try again.` });
|
|
1593
|
+
request.logger.error({ msg: 'Failed to unregister license key', err });
|
|
1594
|
+
|
|
1595
|
+
return h.redirect('/admin/config/license').takeover();
|
|
1596
|
+
},
|
|
1597
|
+
|
|
1598
|
+
payload: Joi.object({})
|
|
1164
1599
|
}
|
|
1165
1600
|
}
|
|
1166
1601
|
});
|
|
1167
1602
|
|
|
1168
|
-
// AI reload models route
|
|
1169
1603
|
server.route({
|
|
1170
1604
|
method: 'POST',
|
|
1171
|
-
path: '/admin/config/
|
|
1605
|
+
path: '/admin/config/license/trial',
|
|
1172
1606
|
async handler(request) {
|
|
1173
1607
|
try {
|
|
1174
|
-
|
|
1608
|
+
// provision new trial license
|
|
1175
1609
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1610
|
+
let headers = {
|
|
1611
|
+
'Content-Type': 'application/json',
|
|
1612
|
+
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
let res = await fetchCmd(`${LICENSE_HOST}/licenses/trial`, {
|
|
1616
|
+
method: 'post',
|
|
1617
|
+
body: JSON.stringify({
|
|
1618
|
+
version: packageData.version,
|
|
1619
|
+
app: '@postalsys/emailengine-app',
|
|
1620
|
+
hostname: os.hostname() || 'localhost',
|
|
1621
|
+
url: (await settings.get('serviceUrl')) || ''
|
|
1622
|
+
}),
|
|
1623
|
+
headers,
|
|
1624
|
+
dispatcher: httpAgent.retry
|
|
1183
1625
|
});
|
|
1184
1626
|
|
|
1185
|
-
if (
|
|
1186
|
-
|
|
1627
|
+
if (!res.ok) {
|
|
1628
|
+
let err = new Error(res.statusText || `Invalid response: ${res.status} ${res.statusText}`);
|
|
1629
|
+
err.statusCode = res.status;
|
|
1630
|
+
|
|
1631
|
+
try {
|
|
1632
|
+
err.response = await res.json();
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
// ignore
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
throw err;
|
|
1187
1638
|
}
|
|
1188
1639
|
|
|
1189
|
-
|
|
1190
|
-
} catch (err) {
|
|
1191
|
-
request.logger.error({ msg: 'Failed reloading OpenAI models', err });
|
|
1192
|
-
return { success: false, error: err.message };
|
|
1193
|
-
}
|
|
1194
|
-
},
|
|
1195
|
-
options: {
|
|
1196
|
-
validate: {
|
|
1197
|
-
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
1198
|
-
failAction,
|
|
1199
|
-
payload: Joi.object({
|
|
1200
|
-
openAiAPIKey: settingsSchema.openAiAPIKey.empty(''),
|
|
1201
|
-
openAiAPIUrl: settingsSchema.openAiAPIUrl.default('')
|
|
1202
|
-
})
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1640
|
+
const data = await res.json();
|
|
1206
1641
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1642
|
+
let licenseFile = `-----BEGIN LICENSE-----
|
|
1643
|
+
${Buffer.from(data.content, 'base64url').toString('base64')}
|
|
1644
|
+
-----END LICENSE-----`;
|
|
1645
|
+
|
|
1646
|
+
const licenseInfo = await call({ cmd: 'updateLicense', license: licenseFile });
|
|
1647
|
+
if (!licenseInfo) {
|
|
1648
|
+
let err = new Error('Failed to update license. Check license file contents.');
|
|
1649
|
+
err.statusCode = 403;
|
|
1650
|
+
err.details = { license: err.message };
|
|
1651
|
+
throw err;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
if (licenseInfo.active) {
|
|
1655
|
+
await request.flash({ type: 'info', message: `Trial activated` });
|
|
1656
|
+
return { success: true, message: `Trial activated` };
|
|
1218
1657
|
}
|
|
1658
|
+
|
|
1659
|
+
throw new Error('Failed to activate provisioned trial license');
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
request.logger.error({ msg: 'Failed to provision a trial license key', err, remoteAddress: request.app.ip });
|
|
1662
|
+
return { success: false, error: (err.response && err.response.error) || err.message };
|
|
1219
1663
|
}
|
|
1220
|
-
return { success: true };
|
|
1221
1664
|
},
|
|
1222
1665
|
options: {
|
|
1223
1666
|
validate: {
|
|
1224
|
-
options: {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
.regex(/^[a-z0-9]{1,5}([-_][a-z0-9]{1,15})?$/)
|
|
1232
|
-
.allow(false),
|
|
1233
|
-
timezone: Joi.string().empty('').allow(false).max(255)
|
|
1234
|
-
})
|
|
1667
|
+
options: {
|
|
1668
|
+
stripUnknown: true,
|
|
1669
|
+
abortEarly: false,
|
|
1670
|
+
convert: true
|
|
1671
|
+
},
|
|
1672
|
+
|
|
1673
|
+
failAction
|
|
1235
1674
|
}
|
|
1236
1675
|
}
|
|
1237
1676
|
});
|