emailengine-app 2.61.1 → 2.61.2
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/CHANGELOG.md +9 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account/account-state.js +248 -0
- package/lib/account.js +17 -178
- package/lib/api-routes/account-routes.js +1006 -0
- package/lib/api-routes/message-routes.js +1377 -0
- package/lib/consts.js +12 -2
- package/lib/email-client/base-client.js +282 -771
- package/lib/email-client/gmail/gmail-api.js +243 -0
- package/lib/email-client/gmail-client.js +145 -53
- package/lib/email-client/imap/mailbox.js +24 -698
- package/lib/email-client/imap/sync-operations.js +812 -0
- package/lib/email-client/imap-client.js +1 -1
- package/lib/email-client/message-builder.js +566 -0
- package/lib/email-client/notification-handler.js +314 -0
- package/lib/email-client/outlook/graph-api.js +326 -0
- package/lib/email-client/outlook-client.js +159 -113
- package/lib/email-client/smtp-pool-manager.js +196 -0
- package/lib/imapproxy/imap-server.js +3 -12
- package/lib/oauth/gmail.js +4 -4
- package/lib/oauth/mail-ru.js +30 -5
- package/lib/oauth/outlook.js +57 -3
- package/lib/oauth/pubsub/google.js +30 -11
- package/lib/oauth/scope-checker.js +202 -0
- package/lib/oauth2-apps.js +8 -4
- package/lib/redis-operations.js +484 -0
- package/lib/routes-ui.js +283 -2582
- package/lib/tools.js +4 -196
- package/lib/ui-routes/account-routes.js +1931 -0
- package/lib/ui-routes/admin-config-routes.js +1233 -0
- package/lib/ui-routes/admin-entities-routes.js +2367 -0
- package/lib/ui-routes/oauth-routes.js +992 -0
- package/lib/utils/network.js +237 -0
- package/package.json +9 -9
- package/sbom.json +1 -1
- package/static/js/app.js +5 -5
- package/static/licenses.html +78 -18
- package/translations/de.mo +0 -0
- package/translations/de.po +85 -82
- package/translations/en.mo +0 -0
- package/translations/en.po +63 -71
- package/translations/et.mo +0 -0
- package/translations/et.po +84 -82
- package/translations/fr.mo +0 -0
- package/translations/fr.po +85 -82
- package/translations/ja.mo +0 -0
- package/translations/ja.po +84 -82
- package/translations/messages.pot +74 -87
- package/translations/nl.mo +0 -0
- package/translations/nl.po +86 -82
- package/translations/pl.mo +0 -0
- package/translations/pl.po +84 -82
- package/views/account/security.hbs +4 -4
- package/views/accounts/account.hbs +13 -13
- package/views/accounts/register/imap-server.hbs +12 -12
- package/views/config/document-store/pre-processing/index.hbs +4 -2
- package/views/config/oauth/app.hbs +6 -7
- package/views/config/oauth/index.hbs +2 -2
- package/views/config/service.hbs +3 -4
- package/views/dashboard.hbs +5 -7
- package/views/error.hbs +22 -7
- package/views/gateways/gateway.hbs +2 -2
- package/views/partials/add_account_modal.hbs +7 -10
- package/views/partials/document_store_header.hbs +1 -1
- package/views/partials/editor_scope_info.hbs +0 -1
- package/views/partials/oauth_config_header.hbs +1 -1
- package/views/partials/side_menu.hbs +3 -3
- package/views/partials/webhook_form.hbs +2 -2
- package/views/templates/index.hbs +1 -1
- package/views/templates/template.hbs +8 -8
- package/views/tokens/index.hbs +6 -6
- package/views/tokens/new.hbs +1 -1
- package/views/webhooks/index.hbs +4 -4
- package/views/webhooks/webhook.hbs +7 -7
- package/workers/api.js +148 -2436
- package/workers/smtp.js +2 -1
- package/lib/imapproxy/imap-core/test/client.js +0 -46
- package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
- package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
- package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
- package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
- package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
- package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
- package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
- package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
- package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
- package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
- package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
- package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
- package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
- package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
- package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
- package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
- package/lib/imapproxy/imap-core/test/test-client.js +0 -152
- package/lib/imapproxy/imap-core/test/test-server.js +0 -623
- package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
- package/test/api-test.js +0 -899
- package/test/autoreply-test.js +0 -327
- package/test/bounce-test.js +0 -151
- package/test/complaint-test.js +0 -256
- package/test/fixtures/autoreply/LICENSE +0 -27
- package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
- package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
- package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
- package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
- package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
- package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
- package/test/fixtures/bounces/163.eml +0 -2521
- package/test/fixtures/bounces/fastmail.eml +0 -242
- package/test/fixtures/bounces/gmail.eml +0 -252
- package/test/fixtures/bounces/hotmail.eml +0 -655
- package/test/fixtures/bounces/mailru.eml +0 -121
- package/test/fixtures/bounces/outlook.eml +0 -1107
- package/test/fixtures/bounces/postfix.eml +0 -101
- package/test/fixtures/bounces/rambler.eml +0 -116
- package/test/fixtures/bounces/workmail.eml +0 -142
- package/test/fixtures/bounces/yahoo.eml +0 -139
- package/test/fixtures/bounces/zoho.eml +0 -83
- package/test/fixtures/bounces/zonemta.eml +0 -100
- package/test/fixtures/complaints/LICENSE +0 -27
- package/test/fixtures/complaints/amazonses.eml +0 -72
- package/test/fixtures/complaints/dmarc.eml +0 -59
- package/test/fixtures/complaints/hotmail.eml +0 -49
- package/test/fixtures/complaints/optout.eml +0 -40
- package/test/fixtures/complaints/standard-arf.eml +0 -68
- package/test/fixtures/complaints/yahoo.eml +0 -68
- package/test/oauth2-apps-test.js +0 -301
- package/test/sendonly-test.js +0 -160
- package/test/test-config.js +0 -34
- package/test/webhooks-server.js +0 -39
|
@@ -0,0 +1,1233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Joi = require('joi');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const he = require('he');
|
|
6
|
+
const { simpleParser } = require('mailparser');
|
|
7
|
+
const libmime = require('libmime');
|
|
8
|
+
|
|
9
|
+
const settings = require('../settings');
|
|
10
|
+
const { redis, submitQueue, notifyQueue, documentsQueue } = require('../db');
|
|
11
|
+
const getSecret = require('../get-secret');
|
|
12
|
+
const { llmPreProcess } = require('../llm-pre-process');
|
|
13
|
+
const { locales } = require('../translations');
|
|
14
|
+
const consts = require('../consts');
|
|
15
|
+
const packageData = require('../../package.json');
|
|
16
|
+
const timezonesList = require('timezones-list').default;
|
|
17
|
+
|
|
18
|
+
const { failAction, getByteSize, formatByteSize, getDuration, readEnvValue, hasEnvValue, retryAgent } = require('../tools');
|
|
19
|
+
|
|
20
|
+
const { settingsSchema } = require('../schemas');
|
|
21
|
+
|
|
22
|
+
const { DEFAULT_MAX_LOG_LINES, DEFAULT_DELIVERY_ATTEMPTS, REDIS_PREFIX, NONCE_BYTES } = consts;
|
|
23
|
+
|
|
24
|
+
const { fetch: fetchCmd } = require('undici');
|
|
25
|
+
|
|
26
|
+
const OPEN_AI_MODELS = [
|
|
27
|
+
{ name: 'GPT-3 (instruct)', id: 'gpt-3.5-turbo-instruct' },
|
|
28
|
+
{ name: 'GPT-3 (chat)', id: 'gpt-3.5-turbo' },
|
|
29
|
+
{ name: 'GPT-4', id: 'gpt-4' }
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const IMAP_INDEXERS = [
|
|
33
|
+
{
|
|
34
|
+
id: 'full',
|
|
35
|
+
name: 'Full (Default): Builds a comprehensive index that detects new, deleted, and updated emails. This method is slower and uses more storage in Redis.'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'fast',
|
|
39
|
+
name: 'Fast: Quickly detects newly received emails with minimal storage usage in Redis. It does not detect updated or deleted emails.'
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const notificationTypes = Object.keys(consts)
|
|
44
|
+
.map(key => {
|
|
45
|
+
if (/_NOTIFY$/.test(key)) {
|
|
46
|
+
return key.replace(/_NOTIFY$/, '');
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
})
|
|
50
|
+
.filter(key => key)
|
|
51
|
+
.map(key => ({
|
|
52
|
+
key,
|
|
53
|
+
name: consts[`${key}_NOTIFY`],
|
|
54
|
+
description: consts[`${key}_DESCRIPTION`]
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const ADMIN_ACCESS_ADDRESSES = hasEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
58
|
+
? readEnvValue('EENGINE_ADMIN_ACCESS_ADDRESSES')
|
|
59
|
+
.split(',')
|
|
60
|
+
.map(v => v.trim())
|
|
61
|
+
.filter(v => v)
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
const MAX_BODY_SIZE = getByteSize(readEnvValue('EENGINE_MAX_BODY_SIZE')) || consts.DEFAULT_MAX_BODY_SIZE;
|
|
65
|
+
const MAX_PAYLOAD_TIMEOUT = getDuration(readEnvValue('EENGINE_MAX_PAYLOAD_TIMEOUT')) || consts.DEFAULT_MAX_PAYLOAD_TIMEOUT;
|
|
66
|
+
|
|
67
|
+
// Validation schemas
|
|
68
|
+
const configWebhooksSchema = {
|
|
69
|
+
webhooksEnabled: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
70
|
+
webhooks: Joi.string()
|
|
71
|
+
.uri({ scheme: ['http', 'https'], allowRelative: false })
|
|
72
|
+
.allow('')
|
|
73
|
+
.example('https://myservice.com/imap/webhooks'),
|
|
74
|
+
notifyAll: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
75
|
+
headersAll: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
76
|
+
notifyHeaders: Joi.string().empty('').trim(),
|
|
77
|
+
notifyText: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
78
|
+
notifyWebSafeHtml: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
79
|
+
notifyTextSize: Joi.alternatives().try(
|
|
80
|
+
Joi.number().empty('').integer().min(0),
|
|
81
|
+
Joi.string().custom((value, helpers) => {
|
|
82
|
+
let nr = getByteSize(value);
|
|
83
|
+
if (typeof nr !== 'number' || nr < 0) {
|
|
84
|
+
return helpers.error('any.invalid');
|
|
85
|
+
}
|
|
86
|
+
return nr;
|
|
87
|
+
}, 'Byte size conversion')
|
|
88
|
+
),
|
|
89
|
+
notifyCalendarEvents: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
90
|
+
inboxNewOnly: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
91
|
+
notifyAttachments: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
92
|
+
notifyAttachmentSize: Joi.alternatives().try(
|
|
93
|
+
Joi.number().empty('').integer().min(0),
|
|
94
|
+
Joi.string().custom((value, helpers) => {
|
|
95
|
+
let nr = getByteSize(value);
|
|
96
|
+
if (typeof nr !== 'number' || nr < 0) {
|
|
97
|
+
return helpers.error('any.invalid');
|
|
98
|
+
}
|
|
99
|
+
return nr;
|
|
100
|
+
}, 'Byte size conversion')
|
|
101
|
+
),
|
|
102
|
+
customHeaders: Joi.string()
|
|
103
|
+
.allow('')
|
|
104
|
+
.trim()
|
|
105
|
+
.max(10 * 1024)
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (let type of notificationTypes) {
|
|
109
|
+
configWebhooksSchema[`notify_${type.name}`] = Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const configLoggingSchema = {
|
|
113
|
+
all: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
114
|
+
maxLogLines: Joi.number().integer().empty('').min(0).max(10000000).default(DEFAULT_MAX_LOG_LINES)
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Helper functions
|
|
118
|
+
async function getOpenAiModels(models, selectedModel) {
|
|
119
|
+
let modelList = (await settings.get('openAiModels')) || structuredClone(models);
|
|
120
|
+
|
|
121
|
+
if (selectedModel && !modelList.find(model => model.id === selectedModel)) {
|
|
122
|
+
modelList.unshift({ name: selectedModel, id: selectedModel });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return modelList.map(model => {
|
|
126
|
+
model.selected = model.id === selectedModel;
|
|
127
|
+
return model;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function getOpenAiError(gt) {
|
|
132
|
+
let openAiErrorData = await redis.get(`${REDIS_PREFIX}:openai:error`);
|
|
133
|
+
if (openAiErrorData) {
|
|
134
|
+
try {
|
|
135
|
+
let { error, time } = JSON.parse(openAiErrorData);
|
|
136
|
+
return { error, time: (gt && gt.dateFns.formatDistance(new Date(time), new Date(), { addSuffix: true })) || time };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function getExampleDocumentsPayloads() {
|
|
145
|
+
const exampleDocumentsPayloads = require('../payload-examples-documents.json');
|
|
146
|
+
let examples = structuredClone(exampleDocumentsPayloads);
|
|
147
|
+
let serviceUrl = await settings.get('serviceUrl');
|
|
148
|
+
for (let example of examples) {
|
|
149
|
+
if (example.serviceUrl) {
|
|
150
|
+
example.serviceUrl = serviceUrl || example.serviceUrl;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return examples;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function init(args) {
|
|
157
|
+
const { server, call } = args;
|
|
158
|
+
|
|
159
|
+
const getDefaultPrompt = async () =>
|
|
160
|
+
await call({
|
|
161
|
+
cmd: 'openAiDefaultPrompt'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Webhooks config routes
|
|
165
|
+
server.route({
|
|
166
|
+
method: 'GET',
|
|
167
|
+
path: '/admin/config/webhooks',
|
|
168
|
+
async handler(request, h) {
|
|
169
|
+
const notifyHeaders = (await settings.get('notifyHeaders')) || [];
|
|
170
|
+
const webhookEvents = (await settings.get('webhookEvents')) || [];
|
|
171
|
+
const notifyText = (await settings.get('notifyText')) || false;
|
|
172
|
+
const notifyWebSafeHtml = (await settings.get('notifyWebSafeHtml')) || false;
|
|
173
|
+
const notifyTextSize = Number(await settings.get('notifyTextSize')) || 0;
|
|
174
|
+
const notifyCalendarEvents = (await settings.get('notifyCalendarEvents')) || false;
|
|
175
|
+
const notifyAttachments = (await settings.get('notifyAttachments')) || false;
|
|
176
|
+
const notifyAttachmentSize = Number(await settings.get('notifyAttachmentSize')) || 0;
|
|
177
|
+
const inboxNewOnly = (await settings.get('inboxNewOnly')) || false;
|
|
178
|
+
const customHeaders = (await settings.get('webhooksCustomHeaders')) || [];
|
|
179
|
+
|
|
180
|
+
let webhooksEnabled = await settings.get('webhooksEnabled');
|
|
181
|
+
let values = {
|
|
182
|
+
webhooksEnabled: webhooksEnabled !== null ? !!webhooksEnabled : false,
|
|
183
|
+
webhooks: (await settings.get('webhooks')) || '',
|
|
184
|
+
notifyAll: webhookEvents.includes('*'),
|
|
185
|
+
inboxNewOnly,
|
|
186
|
+
headersAll: notifyHeaders.includes('*'),
|
|
187
|
+
notifyHeaders: notifyHeaders
|
|
188
|
+
.filter(entry => entry !== '*')
|
|
189
|
+
.map(entry => entry.replace(/^mime|^dkim|-id$|^.|-./gi, c => c.toUpperCase()))
|
|
190
|
+
.join('\n'),
|
|
191
|
+
notifyText,
|
|
192
|
+
notifyWebSafeHtml,
|
|
193
|
+
notifyTextSize: notifyTextSize ? formatByteSize(notifyTextSize) : '',
|
|
194
|
+
notifyCalendarEvents,
|
|
195
|
+
notifyAttachments,
|
|
196
|
+
notifyAttachmentSize: notifyAttachmentSize ? formatByteSize(notifyAttachmentSize) : '',
|
|
197
|
+
customHeaders: []
|
|
198
|
+
.concat(customHeaders || [])
|
|
199
|
+
.map(entry => `${entry.key}: ${entry.value}`.trim())
|
|
200
|
+
.join('\n')
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return h.view(
|
|
204
|
+
'config/webhooks',
|
|
205
|
+
{
|
|
206
|
+
pageTitle: 'Webhooks',
|
|
207
|
+
menuConfig: true,
|
|
208
|
+
menuConfigWebhooks: true,
|
|
209
|
+
notificationTypes: notificationTypes.map(type =>
|
|
210
|
+
Object.assign({}, type, { checked: webhookEvents.includes(type.name), isMessageNew: type.name === 'messageNew' })
|
|
211
|
+
),
|
|
212
|
+
values,
|
|
213
|
+
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
214
|
+
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
215
|
+
},
|
|
216
|
+
{ layout: 'app' }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
server.route({
|
|
222
|
+
method: 'POST',
|
|
223
|
+
path: '/admin/config/webhooks',
|
|
224
|
+
async handler(request, h) {
|
|
225
|
+
try {
|
|
226
|
+
let customHeaders = request.payload.customHeaders
|
|
227
|
+
.split(/[\r\n]+/)
|
|
228
|
+
.map(header => header.trim())
|
|
229
|
+
.filter(header => header)
|
|
230
|
+
.map(line => {
|
|
231
|
+
let sep = line.indexOf(':');
|
|
232
|
+
if (sep >= 0) {
|
|
233
|
+
return { key: line.substring(0, sep).trim(), value: line.substring(sep + 1).trim() };
|
|
234
|
+
}
|
|
235
|
+
return { key: line, value: '' };
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const data = {
|
|
239
|
+
webhooksEnabled: request.payload.webhooksEnabled,
|
|
240
|
+
webhooks: request.payload.webhooks,
|
|
241
|
+
notifyText: request.payload.notifyText,
|
|
242
|
+
notifyWebSafeHtml: request.payload.notifyWebSafeHtml,
|
|
243
|
+
notifyTextSize: request.payload.notifyTextSize || 0,
|
|
244
|
+
notifyCalendarEvents: request.payload.notifyCalendarEvents,
|
|
245
|
+
notifyAttachments: request.payload.notifyAttachments,
|
|
246
|
+
notifyAttachmentSize: request.payload.notifyAttachmentSize,
|
|
247
|
+
inboxNewOnly: request.payload.inboxNewOnly,
|
|
248
|
+
webhookEvents: notificationTypes.filter(type => !!request.payload[`notify_${type.name}`]).map(type => type.name),
|
|
249
|
+
notifyHeaders: (request.payload.notifyHeaders || '')
|
|
250
|
+
.split(/\r?\n/)
|
|
251
|
+
.map(line => line.toLowerCase().trim())
|
|
252
|
+
.filter(line => line),
|
|
253
|
+
webhooksCustomHeaders: customHeaders
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (request.payload.notifyAll) {
|
|
257
|
+
data.webhookEvents.push('*');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (request.payload.headersAll) {
|
|
261
|
+
data.notifyHeaders.push('*');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (let key of Object.keys(data)) {
|
|
265
|
+
await settings.set(key, data[key]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!data.webhooksEnabled) {
|
|
269
|
+
await settings.clear('webhookErrorFlag');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
273
|
+
return h.redirect('/admin/config/webhooks');
|
|
274
|
+
} catch (err) {
|
|
275
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
276
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
277
|
+
|
|
278
|
+
return h.view(
|
|
279
|
+
'config/webhooks',
|
|
280
|
+
{
|
|
281
|
+
pageTitle: 'Webhooks',
|
|
282
|
+
menuConfig: true,
|
|
283
|
+
menuConfigWebhooks: true,
|
|
284
|
+
notificationTypes: notificationTypes.map(type =>
|
|
285
|
+
Object.assign({}, type, { checked: !!request.payload[`notify_${type.name}`], isMessageNew: type.name === 'messageNew' })
|
|
286
|
+
),
|
|
287
|
+
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
288
|
+
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
289
|
+
},
|
|
290
|
+
{ layout: 'app' }
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
options: {
|
|
295
|
+
validate: {
|
|
296
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
297
|
+
async failAction(request, h, err) {
|
|
298
|
+
let errors = {};
|
|
299
|
+
if (err.details) {
|
|
300
|
+
err.details.forEach(detail => {
|
|
301
|
+
if (!errors[detail.path]) {
|
|
302
|
+
errors[detail.path] = detail.message;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
308
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
309
|
+
|
|
310
|
+
return h
|
|
311
|
+
.view(
|
|
312
|
+
'config/webhooks',
|
|
313
|
+
{
|
|
314
|
+
pageTitle: 'Webhooks',
|
|
315
|
+
menuConfig: true,
|
|
316
|
+
menuConfigWebhooks: true,
|
|
317
|
+
notificationTypes: notificationTypes.map(type =>
|
|
318
|
+
Object.assign({}, type, {
|
|
319
|
+
checked: !!request.payload[`notify_${type.name}`],
|
|
320
|
+
isMessageNew: type.name === 'messageNew',
|
|
321
|
+
error: errors[`notify_${type.name}`]
|
|
322
|
+
})
|
|
323
|
+
),
|
|
324
|
+
errors,
|
|
325
|
+
webhookErrorFlag: await settings.get('webhookErrorFlag'),
|
|
326
|
+
documentStoreEnabled: (await settings.get('documentStoreEnabled')) || false
|
|
327
|
+
},
|
|
328
|
+
{ layout: 'app' }
|
|
329
|
+
)
|
|
330
|
+
.takeover();
|
|
331
|
+
},
|
|
332
|
+
payload: Joi.object(configWebhooksSchema)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Service config routes
|
|
338
|
+
server.route({
|
|
339
|
+
method: 'GET',
|
|
340
|
+
path: '/admin/config/service',
|
|
341
|
+
async handler(request, h) {
|
|
342
|
+
let trackSentMessages = (await settings.get('trackSentMessages')) || false;
|
|
343
|
+
|
|
344
|
+
const values = {
|
|
345
|
+
serviceUrl: (await settings.get('serviceUrl')) || null,
|
|
346
|
+
serviceSecret: (await settings.get('serviceSecret')) || null,
|
|
347
|
+
queueKeep: (await settings.get('queueKeep')) || 0,
|
|
348
|
+
deliveryAttempts: await settings.get('deliveryAttempts'),
|
|
349
|
+
imapIndexer: (await settings.get('imapIndexer')) || 'full',
|
|
350
|
+
pageBrandName: (await settings.get('pageBrandName')) || '',
|
|
351
|
+
templateHeader: (await settings.get('templateHeader')) || '',
|
|
352
|
+
templateHtmlHead: (await settings.get('templateHtmlHead')) || '',
|
|
353
|
+
scriptEnv: (await settings.get('scriptEnv')) || '',
|
|
354
|
+
enableTokens: !(await settings.get('disableTokens')),
|
|
355
|
+
enableApiProxy: (await settings.get('enableApiProxy')) || false,
|
|
356
|
+
trackClicks: await settings.get('trackClicks'),
|
|
357
|
+
trackOpens: await settings.get('trackOpens'),
|
|
358
|
+
resolveGmailCategories: (await settings.get('resolveGmailCategories')) || false,
|
|
359
|
+
enableOAuthTokensApi: (await settings.get('enableOAuthTokensApi')) || false,
|
|
360
|
+
ignoreMailCertErrors: (await settings.get('ignoreMailCertErrors')) || false,
|
|
361
|
+
locale: (await settings.get('locale')) || false,
|
|
362
|
+
timezone: (await settings.get('timezone')) || false
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
if (typeof values.trackClicks !== 'boolean') {
|
|
366
|
+
values.trackClicks = trackSentMessages;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (typeof values.trackOpens !== 'boolean') {
|
|
370
|
+
values.trackOpens = trackSentMessages;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (typeof values.deliveryAttempts !== 'number') {
|
|
374
|
+
values.deliveryAttempts = DEFAULT_DELIVERY_ATTEMPTS;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return h.view(
|
|
378
|
+
'config/service',
|
|
379
|
+
{
|
|
380
|
+
pageTitle: 'General Settings',
|
|
381
|
+
menuConfig: true,
|
|
382
|
+
menuConfigService: true,
|
|
383
|
+
encryption: await getSecret(),
|
|
384
|
+
locales: locales.map(locale => Object.assign({ selected: locale.locale === values.locale }, locale)),
|
|
385
|
+
timezones: timezonesList.map(entry => ({
|
|
386
|
+
name: entry.label,
|
|
387
|
+
timezone: entry.tzCode,
|
|
388
|
+
selected: entry.tzCode === values.timezone
|
|
389
|
+
})),
|
|
390
|
+
imapIndexers: structuredClone(IMAP_INDEXERS).map(entry => {
|
|
391
|
+
if (entry.id === values.imapIndexer) {
|
|
392
|
+
entry.selected = true;
|
|
393
|
+
}
|
|
394
|
+
return entry;
|
|
395
|
+
}),
|
|
396
|
+
adminAccessLimit: ADMIN_ACCESS_ADDRESSES && ADMIN_ACCESS_ADDRESSES.length,
|
|
397
|
+
values
|
|
398
|
+
},
|
|
399
|
+
{ layout: 'app' }
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
server.route({
|
|
405
|
+
method: 'POST',
|
|
406
|
+
path: '/admin/config/service',
|
|
407
|
+
async handler(request, h) {
|
|
408
|
+
try {
|
|
409
|
+
let data = {
|
|
410
|
+
serviceSecret: request.payload.serviceSecret,
|
|
411
|
+
queueKeep: request.payload.queueKeep,
|
|
412
|
+
pageBrandName: request.payload.pageBrandName,
|
|
413
|
+
templateHeader: request.payload.templateHeader,
|
|
414
|
+
templateHtmlHead: request.payload.templateHtmlHead,
|
|
415
|
+
scriptEnv: request.payload.scriptEnv,
|
|
416
|
+
disableTokens: !request.payload.enableTokens,
|
|
417
|
+
enableApiProxy: request.payload.enableApiProxy,
|
|
418
|
+
trackOpens: request.payload.trackOpens,
|
|
419
|
+
trackClicks: request.payload.trackClicks,
|
|
420
|
+
resolveGmailCategories: request.payload.resolveGmailCategories,
|
|
421
|
+
enableOAuthTokensApi: request.payload.enableOAuthTokensApi,
|
|
422
|
+
ignoreMailCertErrors: request.payload.ignoreMailCertErrors,
|
|
423
|
+
locale: request.payload.locale,
|
|
424
|
+
timezone: request.payload.timezone,
|
|
425
|
+
deliveryAttempts: request.payload.deliveryAttempts,
|
|
426
|
+
imapIndexer: request.payload.imapIndexer
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
if (request.payload.serviceUrl) {
|
|
430
|
+
let url = new URL(request.payload.serviceUrl);
|
|
431
|
+
data.serviceUrl = url.origin;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
for (let key of Object.keys(data)) {
|
|
435
|
+
await settings.set(key, data[key]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
439
|
+
return h.redirect('/admin/config/service');
|
|
440
|
+
} catch (err) {
|
|
441
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
442
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
443
|
+
|
|
444
|
+
return h.view(
|
|
445
|
+
'config/service',
|
|
446
|
+
{
|
|
447
|
+
pageTitle: 'General Settings',
|
|
448
|
+
menuConfig: true,
|
|
449
|
+
menuConfigService: true,
|
|
450
|
+
locales: locales.map(locale => Object.assign({ selected: locale.locale === request.payload.locale }, locale)),
|
|
451
|
+
encryption: await getSecret(),
|
|
452
|
+
timezones: timezonesList.map(entry => ({
|
|
453
|
+
name: entry.label,
|
|
454
|
+
timezone: entry.tzCode,
|
|
455
|
+
selected: entry.tzCode === request.payload.timezone
|
|
456
|
+
})),
|
|
457
|
+
imapIndexers: structuredClone(IMAP_INDEXERS).map(entry => {
|
|
458
|
+
if (entry.id === request.payload.imapIndexer) {
|
|
459
|
+
entry.selected = true;
|
|
460
|
+
}
|
|
461
|
+
return entry;
|
|
462
|
+
}),
|
|
463
|
+
adminAccessLimit: ADMIN_ACCESS_ADDRESSES && ADMIN_ACCESS_ADDRESSES.length
|
|
464
|
+
},
|
|
465
|
+
{ layout: 'app' }
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
options: {
|
|
470
|
+
validate: {
|
|
471
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
472
|
+
async failAction(request, h, err) {
|
|
473
|
+
let errors = {};
|
|
474
|
+
if (err.details) {
|
|
475
|
+
err.details.forEach(detail => {
|
|
476
|
+
if (!errors[detail.path]) {
|
|
477
|
+
errors[detail.path] = detail.message;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
483
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
484
|
+
|
|
485
|
+
return h
|
|
486
|
+
.view(
|
|
487
|
+
'config/service',
|
|
488
|
+
{
|
|
489
|
+
pageTitle: 'General Settings',
|
|
490
|
+
menuConfig: true,
|
|
491
|
+
menuConfigService: true,
|
|
492
|
+
locales: locales.map(locale => Object.assign({ selected: locale.locale === request.payload.locale }, locale)),
|
|
493
|
+
encryption: await getSecret(),
|
|
494
|
+
timezones: timezonesList.map(entry => ({
|
|
495
|
+
name: entry.label,
|
|
496
|
+
timezone: entry.tzCode,
|
|
497
|
+
selected: entry.tzCode === request.payload.timezone
|
|
498
|
+
})),
|
|
499
|
+
imapIndexers: structuredClone(IMAP_INDEXERS).map(entry => {
|
|
500
|
+
if (entry.id === request.payload.imapIndexer) {
|
|
501
|
+
entry.selected = true;
|
|
502
|
+
}
|
|
503
|
+
return entry;
|
|
504
|
+
}),
|
|
505
|
+
adminAccessLimit: ADMIN_ACCESS_ADDRESSES && ADMIN_ACCESS_ADDRESSES.length,
|
|
506
|
+
errors
|
|
507
|
+
},
|
|
508
|
+
{ layout: 'app' }
|
|
509
|
+
)
|
|
510
|
+
.takeover();
|
|
511
|
+
},
|
|
512
|
+
payload: Joi.object({
|
|
513
|
+
serviceUrl: settingsSchema.serviceUrl,
|
|
514
|
+
serviceSecret: settingsSchema.serviceSecret,
|
|
515
|
+
queueKeep: settingsSchema.queueKeep.default(0),
|
|
516
|
+
deliveryAttempts: settingsSchema.deliveryAttempts.default(DEFAULT_DELIVERY_ATTEMPTS),
|
|
517
|
+
imapIndexer: settingsSchema.imapIndexer.default('full'),
|
|
518
|
+
pageBrandName: settingsSchema.pageBrandName.default(''),
|
|
519
|
+
templateHeader: settingsSchema.templateHeader.default(''),
|
|
520
|
+
templateHtmlHead: settingsSchema.templateHtmlHead.default(''),
|
|
521
|
+
scriptEnv: settingsSchema.scriptEnv.default(''),
|
|
522
|
+
enableApiProxy: settingsSchema.enableApiProxy.default(false),
|
|
523
|
+
trackOpens: settingsSchema.trackOpens.default(false),
|
|
524
|
+
trackClicks: settingsSchema.trackClicks.default(false),
|
|
525
|
+
resolveGmailCategories: settingsSchema.resolveGmailCategories.default(false),
|
|
526
|
+
ignoreMailCertErrors: settingsSchema.ignoreMailCertErrors.default(false),
|
|
527
|
+
enableOAuthTokensApi: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
528
|
+
enableTokens: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),
|
|
529
|
+
locale: settingsSchema.locale
|
|
530
|
+
.empty('')
|
|
531
|
+
.valid(...locales.map(locale => locale.locale))
|
|
532
|
+
.default('en'),
|
|
533
|
+
timezone: settingsSchema.timezone.empty('')
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Service preview route
|
|
540
|
+
server.route({
|
|
541
|
+
method: 'POST',
|
|
542
|
+
path: '/admin/config/service/preview',
|
|
543
|
+
async handler(request, h) {
|
|
544
|
+
return h.view(
|
|
545
|
+
'config/service-preview',
|
|
546
|
+
{
|
|
547
|
+
pageBrandName: request.payload.pageBrandName,
|
|
548
|
+
embeddedTemplateHeader: request.payload.templateHeader,
|
|
549
|
+
embeddedTemplateHtmlHead: request.payload.templateHtmlHead
|
|
550
|
+
},
|
|
551
|
+
{ layout: 'public' }
|
|
552
|
+
);
|
|
553
|
+
},
|
|
554
|
+
options: {
|
|
555
|
+
validate: {
|
|
556
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
557
|
+
async failAction(request, h, err) {
|
|
558
|
+
request.logger.error({ msg: 'Failed to process preview', err });
|
|
559
|
+
return h.redirect('/admin').takeover();
|
|
560
|
+
},
|
|
561
|
+
payload: Joi.object({
|
|
562
|
+
pageBrandName: settingsSchema.pageBrandName.default(''),
|
|
563
|
+
templateHeader: settingsSchema.templateHeader.default(''),
|
|
564
|
+
templateHtmlHead: settingsSchema.templateHtmlHead.default('')
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Clear error route
|
|
571
|
+
server.route({
|
|
572
|
+
method: 'POST',
|
|
573
|
+
path: '/admin/config/clear-error',
|
|
574
|
+
async handler(request) {
|
|
575
|
+
switch (request.payload.alert) {
|
|
576
|
+
case 'open-ai':
|
|
577
|
+
await redis.del(`${REDIS_PREFIX}:openai:error`);
|
|
578
|
+
break;
|
|
579
|
+
case 'webhook-default':
|
|
580
|
+
await settings.clear('webhookErrorFlag');
|
|
581
|
+
break;
|
|
582
|
+
case 'webhook-route':
|
|
583
|
+
if (request.payload.entry) {
|
|
584
|
+
await redis.hdel(`${REDIS_PREFIX}wh:c`, `${request.payload.entry}:webhookErrorFlag`);
|
|
585
|
+
}
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
return { success: true };
|
|
589
|
+
},
|
|
590
|
+
options: {
|
|
591
|
+
validate: {
|
|
592
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
593
|
+
failAction,
|
|
594
|
+
payload: Joi.object({
|
|
595
|
+
alert: Joi.string().required().max(1024),
|
|
596
|
+
entry: Joi.string().empty('').max(1024).trim()
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Service clean route
|
|
603
|
+
server.route({
|
|
604
|
+
method: 'POST',
|
|
605
|
+
path: '/admin/config/service/clean',
|
|
606
|
+
async handler(request) {
|
|
607
|
+
let errors = [];
|
|
608
|
+
for (let queue of [submitQueue, notifyQueue, documentsQueue]) {
|
|
609
|
+
for (let type of ['failed', 'completed']) {
|
|
610
|
+
try {
|
|
611
|
+
await queue.clean(1000, 100000, type);
|
|
612
|
+
request.logger.trace({ msg: 'Queue cleaned', queue: queue.name, type });
|
|
613
|
+
} catch (err) {
|
|
614
|
+
request.logger.error({ msg: 'Failed to clean queue', queue: queue.name, type, err });
|
|
615
|
+
errors.push(err.message);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (errors.length) {
|
|
621
|
+
return { success: false, error: 'Cleaning failed for some queues' };
|
|
622
|
+
}
|
|
623
|
+
return { success: true };
|
|
624
|
+
},
|
|
625
|
+
options: {
|
|
626
|
+
validate: {
|
|
627
|
+
options: { stripUnknown: true, abortEarly: false, convert: true }
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Logging config routes
|
|
633
|
+
server.route({
|
|
634
|
+
method: 'GET',
|
|
635
|
+
path: '/admin/config/logging',
|
|
636
|
+
async handler(request, h) {
|
|
637
|
+
let values = (await settings.get('logs')) || {};
|
|
638
|
+
if (typeof values.maxLogLines === 'undefined') {
|
|
639
|
+
values.maxLogLines = DEFAULT_MAX_LOG_LINES;
|
|
640
|
+
}
|
|
641
|
+
values.accounts = [].concat(values.accounts || []).join('\n');
|
|
642
|
+
|
|
643
|
+
return h.view(
|
|
644
|
+
'config/logging',
|
|
645
|
+
{
|
|
646
|
+
pageTitle: 'Logging',
|
|
647
|
+
menuConfig: true,
|
|
648
|
+
menuConfigLogging: true,
|
|
649
|
+
values
|
|
650
|
+
},
|
|
651
|
+
{ layout: 'app' }
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
server.route({
|
|
657
|
+
method: 'POST',
|
|
658
|
+
path: '/admin/config/logging',
|
|
659
|
+
async handler(request, h) {
|
|
660
|
+
try {
|
|
661
|
+
const data = {
|
|
662
|
+
logs: {
|
|
663
|
+
all: !!request.payload.all,
|
|
664
|
+
maxLogLines: request.payload.maxLogLines || 0
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
for (let key of Object.keys(data)) {
|
|
669
|
+
await settings.set(key, data[key]);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
673
|
+
return h.redirect('/admin/config/logging');
|
|
674
|
+
} catch (err) {
|
|
675
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
676
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
677
|
+
|
|
678
|
+
return h.view(
|
|
679
|
+
'config/logging',
|
|
680
|
+
{
|
|
681
|
+
pageTitle: 'Logging',
|
|
682
|
+
menuConfig: true,
|
|
683
|
+
menuConfigWebhooks: true
|
|
684
|
+
},
|
|
685
|
+
{ layout: 'app' }
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
options: {
|
|
690
|
+
validate: {
|
|
691
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
692
|
+
async failAction(request, h, err) {
|
|
693
|
+
let errors = {};
|
|
694
|
+
if (err.details) {
|
|
695
|
+
err.details.forEach(detail => {
|
|
696
|
+
if (!errors[detail.path]) {
|
|
697
|
+
errors[detail.path] = detail.message;
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
703
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
704
|
+
|
|
705
|
+
return h
|
|
706
|
+
.view(
|
|
707
|
+
'config/logging',
|
|
708
|
+
{
|
|
709
|
+
pageTitle: 'Logging',
|
|
710
|
+
menuConfig: true,
|
|
711
|
+
menuConfigWebhooks: true,
|
|
712
|
+
errors
|
|
713
|
+
},
|
|
714
|
+
{ layout: 'app' }
|
|
715
|
+
)
|
|
716
|
+
.takeover();
|
|
717
|
+
},
|
|
718
|
+
payload: Joi.object(configLoggingSchema)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Logging reconnect route
|
|
724
|
+
server.route({
|
|
725
|
+
method: 'POST',
|
|
726
|
+
path: '/admin/config/logging/reconnect',
|
|
727
|
+
async handler(request) {
|
|
728
|
+
try {
|
|
729
|
+
let requested = 0;
|
|
730
|
+
for (let account of request.payload.accounts) {
|
|
731
|
+
request.logger.info({ msg: 'Request reconnect for logging', account });
|
|
732
|
+
try {
|
|
733
|
+
await call({ cmd: 'update', account });
|
|
734
|
+
requested++;
|
|
735
|
+
} catch (err) {
|
|
736
|
+
request.logger.error({ msg: 'Reconnect request failed', action: 'request_reconnect', account, err });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return { success: true, accounts: requested };
|
|
741
|
+
} catch (err) {
|
|
742
|
+
request.logger.error({ msg: 'Failed to request reconnect', err, accounts: request.payload.accounts });
|
|
743
|
+
return { success: false, error: err.message };
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
options: {
|
|
747
|
+
validate: {
|
|
748
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
749
|
+
failAction,
|
|
750
|
+
payload: Joi.object({
|
|
751
|
+
accounts: Joi.array().items(Joi.string().max(256)).default([]).label('LoggedAccounts')
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Webhooks test route
|
|
758
|
+
server.route({
|
|
759
|
+
method: 'POST',
|
|
760
|
+
path: '/admin/config/webhooks/test',
|
|
761
|
+
async handler(request) {
|
|
762
|
+
let headers = {
|
|
763
|
+
'Content-Type': 'application/json',
|
|
764
|
+
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const webhooks = request.payload.webhooks;
|
|
768
|
+
if (!webhooks) {
|
|
769
|
+
return { success: false, target: webhooks, error: 'Webhook URL is not set' };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
let parsed = new URL(webhooks);
|
|
773
|
+
let username, password;
|
|
774
|
+
|
|
775
|
+
if (parsed.username) {
|
|
776
|
+
username = he.decode(parsed.username);
|
|
777
|
+
parsed.username = '';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (parsed.password) {
|
|
781
|
+
password = he.decode(parsed.password);
|
|
782
|
+
parsed.password = '';
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (username || password) {
|
|
786
|
+
headers.Authorization = `Basic ${Buffer.from(he.encode(username || '') + ':' + he.encode(password || '')).toString('base64')}`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
let customHeaders = request.payload.customHeaders
|
|
790
|
+
.split(/[\r\n]+/)
|
|
791
|
+
.map(header => header.trim())
|
|
792
|
+
.filter(header => header)
|
|
793
|
+
.map(line => {
|
|
794
|
+
let sep = line.indexOf(':');
|
|
795
|
+
if (sep >= 0) {
|
|
796
|
+
return { key: line.substring(0, sep).trim(), value: line.substring(sep + 1).trim() };
|
|
797
|
+
}
|
|
798
|
+
return { key: line, value: '' };
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
customHeaders.forEach(header => {
|
|
802
|
+
headers[header.key] = header.value;
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
let start = Date.now();
|
|
806
|
+
let duration;
|
|
807
|
+
try {
|
|
808
|
+
let res;
|
|
809
|
+
let serviceUrl = await settings.get('serviceUrl');
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
res = await fetchCmd(parsed.toString(), {
|
|
813
|
+
method: 'post',
|
|
814
|
+
body:
|
|
815
|
+
request.payload.payload ||
|
|
816
|
+
JSON.stringify({
|
|
817
|
+
serviceUrl,
|
|
818
|
+
account: null,
|
|
819
|
+
date: new Date().toISOString(),
|
|
820
|
+
event: 'test',
|
|
821
|
+
data: { nonce: crypto.randomBytes(NONCE_BYTES).toString('base64url') }
|
|
822
|
+
}),
|
|
823
|
+
headers,
|
|
824
|
+
dispatcher: retryAgent
|
|
825
|
+
});
|
|
826
|
+
duration = Date.now() - start;
|
|
827
|
+
} catch (err) {
|
|
828
|
+
duration = Date.now() - start;
|
|
829
|
+
throw err.cause || err;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (!res.ok) {
|
|
833
|
+
let err = new Error(res.statusText || `Invalid response: ${res.status} ${res.statusText}`);
|
|
834
|
+
err.statusCode = res.status;
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return { success: true, target: webhooks, duration };
|
|
839
|
+
} catch (err) {
|
|
840
|
+
request.logger.error({ msg: 'Failed posting webhook', webhooks, event: 'test', err });
|
|
841
|
+
return { success: false, target: webhooks, duration, error: err.message, code: err.code };
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
options: {
|
|
845
|
+
tags: ['test'],
|
|
846
|
+
validate: {
|
|
847
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
848
|
+
failAction,
|
|
849
|
+
payload: Joi.object({
|
|
850
|
+
webhooks: Joi.string()
|
|
851
|
+
.uri({ scheme: ['http', 'https'], allowRelative: false })
|
|
852
|
+
.allow(''),
|
|
853
|
+
customHeaders: Joi.string()
|
|
854
|
+
.allow('')
|
|
855
|
+
.trim()
|
|
856
|
+
.max(10 * 1024)
|
|
857
|
+
.default(''),
|
|
858
|
+
payload: Joi.string()
|
|
859
|
+
.max(1024 * 1024)
|
|
860
|
+
.empty('')
|
|
861
|
+
.trim()
|
|
862
|
+
})
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// AI config routes
|
|
868
|
+
server.route({
|
|
869
|
+
method: 'GET',
|
|
870
|
+
path: '/admin/config/ai',
|
|
871
|
+
async handler(request, h) {
|
|
872
|
+
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
873
|
+
if (entry.error && typeof entry.error === 'string') {
|
|
874
|
+
entry.error = entry.error
|
|
875
|
+
.replace(/\r?\n/g, '\n')
|
|
876
|
+
.replace(/^\s+at\s+.*$/gm, '')
|
|
877
|
+
.replace(/\n+/g, '\n')
|
|
878
|
+
.trim()
|
|
879
|
+
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
880
|
+
}
|
|
881
|
+
return entry;
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const values = {
|
|
885
|
+
generateEmailSummary: (await settings.get('generateEmailSummary')) || false,
|
|
886
|
+
openAiPrompt: ((await settings.get('openAiPrompt')) || '').toString(),
|
|
887
|
+
contentFnJson: JSON.stringify(
|
|
888
|
+
((await settings.get(`openAiPreProcessingFn`)) || '').toString() ||
|
|
889
|
+
`// Pass all emails
|
|
890
|
+
return true;`
|
|
891
|
+
),
|
|
892
|
+
openAiAPIUrl: ((await settings.get('openAiAPIUrl')) || '').toString(),
|
|
893
|
+
openAiTemperature: ((await settings.get('openAiTemperature')) || '').toString(),
|
|
894
|
+
openAiTopP: ((await settings.get('openAiTopP')) || '').toString(),
|
|
895
|
+
openAiMaxTokens: ((await settings.get('openAiMaxTokens')) || '').toString()
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
if (!values.openAiPrompt.trim()) {
|
|
899
|
+
values.openAiPrompt = await getDefaultPrompt();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
903
|
+
let openAiModel = await settings.get('openAiModel');
|
|
904
|
+
let openAiError = await getOpenAiError(request.app.gt);
|
|
905
|
+
|
|
906
|
+
return h.view(
|
|
907
|
+
'config/ai',
|
|
908
|
+
{
|
|
909
|
+
pageTitle: 'AI Processing',
|
|
910
|
+
menuConfig: true,
|
|
911
|
+
menuConfigAi: true,
|
|
912
|
+
errorLog,
|
|
913
|
+
defaultPromptJson: JSON.stringify({ prompt: await getDefaultPrompt() }),
|
|
914
|
+
values,
|
|
915
|
+
hasOpenAiAPIKey,
|
|
916
|
+
openAiError,
|
|
917
|
+
openAiModels: await getOpenAiModels(OPEN_AI_MODELS, openAiModel),
|
|
918
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'),
|
|
919
|
+
examplePayloadsJson: JSON.stringify(
|
|
920
|
+
(await getExampleDocumentsPayloads()).map(entry =>
|
|
921
|
+
Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined })
|
|
922
|
+
)
|
|
923
|
+
)
|
|
924
|
+
},
|
|
925
|
+
{ layout: 'app' }
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
server.route({
|
|
931
|
+
method: 'POST',
|
|
932
|
+
path: '/admin/config/ai',
|
|
933
|
+
async handler(request, h) {
|
|
934
|
+
try {
|
|
935
|
+
let contentFn;
|
|
936
|
+
try {
|
|
937
|
+
if (request.payload.contentFnJson === '') {
|
|
938
|
+
contentFn = null;
|
|
939
|
+
} else {
|
|
940
|
+
contentFn = JSON.parse(request.payload.contentFnJson);
|
|
941
|
+
if (typeof contentFn !== 'string') {
|
|
942
|
+
throw new Error('Invalid Format');
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
} catch (err) {
|
|
946
|
+
err.details = { contentFnJson: 'Invalid JSON' };
|
|
947
|
+
throw err;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
let data = {
|
|
951
|
+
generateEmailSummary: request.payload.generateEmailSummary,
|
|
952
|
+
openAiModel: request.payload.openAiModel,
|
|
953
|
+
openAiAPIUrl: request.payload.openAiAPIUrl,
|
|
954
|
+
openAiPrompt: (request.payload.openAiPrompt || '').toString(),
|
|
955
|
+
openAiPreProcessingFn: contentFn,
|
|
956
|
+
openAiTemperature: request.payload.openAiTemperature,
|
|
957
|
+
openAiTopP: request.payload.openAiTopP,
|
|
958
|
+
openAiMaxTokens: request.payload.openAiMaxTokens
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
let defaultUserPrompt = await getDefaultPrompt();
|
|
962
|
+
if (!data.openAiPrompt.trim() || data.openAiPrompt.trim() === defaultUserPrompt.trim()) {
|
|
963
|
+
data.openAiPrompt = '';
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (typeof request.payload.openAiAPIKey === 'string') {
|
|
967
|
+
data.openAiAPIKey = request.payload.openAiAPIKey;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (typeof request.payload.openAiAPIUrl === 'string') {
|
|
971
|
+
data.openAiAPIUrl = request.payload.openAiAPIUrl;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
for (let key of Object.keys(data)) {
|
|
975
|
+
await settings.set(key, data[key]);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
await request.flash({ type: 'info', message: `Configuration updated` });
|
|
979
|
+
return h.redirect('/admin/config/ai');
|
|
980
|
+
} catch (err) {
|
|
981
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
982
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
983
|
+
|
|
984
|
+
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
985
|
+
let openAiError = await getOpenAiError(request.app.gt);
|
|
986
|
+
|
|
987
|
+
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
988
|
+
if (entry.error && typeof entry.error === 'string') {
|
|
989
|
+
entry.error = entry.error
|
|
990
|
+
.replace(/\r?\n/g, '\n')
|
|
991
|
+
.replace(/^\s+at\s+.*$/gm, '')
|
|
992
|
+
.replace(/\n+/g, '\n')
|
|
993
|
+
.trim()
|
|
994
|
+
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
995
|
+
}
|
|
996
|
+
return entry;
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
return h.view(
|
|
1000
|
+
'config/ai',
|
|
1001
|
+
{
|
|
1002
|
+
pageTitle: 'AI Processing',
|
|
1003
|
+
menuConfig: true,
|
|
1004
|
+
menuConfigAi: true,
|
|
1005
|
+
errorLog,
|
|
1006
|
+
defaultPromptJson: JSON.stringify({ prompt: await getDefaultPrompt() }),
|
|
1007
|
+
hasOpenAiAPIKey,
|
|
1008
|
+
openAiError,
|
|
1009
|
+
openAiModels: await getOpenAiModels(OPEN_AI_MODELS, request.payload.openAiModel),
|
|
1010
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'),
|
|
1011
|
+
examplePayloadsJson: JSON.stringify(
|
|
1012
|
+
(await getExampleDocumentsPayloads()).map(entry =>
|
|
1013
|
+
Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined })
|
|
1014
|
+
)
|
|
1015
|
+
)
|
|
1016
|
+
},
|
|
1017
|
+
{ layout: 'app' }
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
options: {
|
|
1022
|
+
validate: {
|
|
1023
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
1024
|
+
async failAction(request, h, err) {
|
|
1025
|
+
let errors = {};
|
|
1026
|
+
if (err.details) {
|
|
1027
|
+
err.details.forEach(detail => {
|
|
1028
|
+
if (!errors[detail.path]) {
|
|
1029
|
+
errors[detail.path] = detail.message;
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
await request.flash({ type: 'danger', message: `Couldn't save settings. Try again.` });
|
|
1035
|
+
request.logger.error({ msg: 'Failed to update configuration', err });
|
|
1036
|
+
|
|
1037
|
+
let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey'));
|
|
1038
|
+
let openAiError = await getOpenAiError(request.app.gt);
|
|
1039
|
+
|
|
1040
|
+
const errorLog = ((await llmPreProcess.getErrorLog()) || []).map(entry => {
|
|
1041
|
+
if (entry.error && typeof entry.error === 'string') {
|
|
1042
|
+
entry.error = entry.error
|
|
1043
|
+
.replace(/\r?\n/g, '\n')
|
|
1044
|
+
.replace(/^\s+at\s+.*$/gm, '')
|
|
1045
|
+
.replace(/\n+/g, '\n')
|
|
1046
|
+
.trim()
|
|
1047
|
+
.replace(/(evalmachine.<anonymous>:)(\d+)/, (o, p, n) => p + (Number(n) - 1));
|
|
1048
|
+
}
|
|
1049
|
+
return entry;
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
return h
|
|
1053
|
+
.view(
|
|
1054
|
+
'config/ai',
|
|
1055
|
+
{
|
|
1056
|
+
pageTitle: 'AI Processing',
|
|
1057
|
+
menuConfig: true,
|
|
1058
|
+
menuConfigAi: true,
|
|
1059
|
+
errorLog,
|
|
1060
|
+
defaultPromptJson: JSON.stringify({ prompt: await getDefaultPrompt() }),
|
|
1061
|
+
hasOpenAiAPIKey,
|
|
1062
|
+
openAiError,
|
|
1063
|
+
openAiModels: await getOpenAiModels(OPEN_AI_MODELS, request.payload.openAiModel),
|
|
1064
|
+
scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'),
|
|
1065
|
+
examplePayloadsJson: JSON.stringify(
|
|
1066
|
+
(await getExampleDocumentsPayloads()).map(entry =>
|
|
1067
|
+
Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined })
|
|
1068
|
+
)
|
|
1069
|
+
),
|
|
1070
|
+
errors
|
|
1071
|
+
},
|
|
1072
|
+
{ layout: 'app' }
|
|
1073
|
+
)
|
|
1074
|
+
.takeover();
|
|
1075
|
+
},
|
|
1076
|
+
payload: Joi.object({
|
|
1077
|
+
generateEmailSummary: settingsSchema.generateEmailSummary.default(false),
|
|
1078
|
+
openAiAPIKey: settingsSchema.openAiAPIKey.empty(''),
|
|
1079
|
+
openAiModel: settingsSchema.openAiModel.empty(''),
|
|
1080
|
+
openAiAPIUrl: settingsSchema.openAiAPIUrl.default(''),
|
|
1081
|
+
openAiPrompt: settingsSchema.openAiPrompt.default(''),
|
|
1082
|
+
contentFnJson: Joi.string()
|
|
1083
|
+
.max(1024 * 1024)
|
|
1084
|
+
.default('')
|
|
1085
|
+
.allow('')
|
|
1086
|
+
.trim(),
|
|
1087
|
+
openAiTemperature: settingsSchema.openAiTemperature.default(''),
|
|
1088
|
+
openAiTopP: settingsSchema.openAiTopP.default(''),
|
|
1089
|
+
openAiMaxTokens: settingsSchema.openAiMaxTokens.default('')
|
|
1090
|
+
})
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
// AI test prompt route
|
|
1096
|
+
server.route({
|
|
1097
|
+
method: 'POST',
|
|
1098
|
+
path: '/admin/config/ai/test-prompt',
|
|
1099
|
+
async handler(request) {
|
|
1100
|
+
try {
|
|
1101
|
+
request.logger.info({ msg: 'Prompt test' });
|
|
1102
|
+
|
|
1103
|
+
const parsed = await simpleParser(Buffer.from(request.payload.emailFile, 'base64'));
|
|
1104
|
+
|
|
1105
|
+
const response = {};
|
|
1106
|
+
response.summary = await call({
|
|
1107
|
+
cmd: 'generateSummary',
|
|
1108
|
+
data: {
|
|
1109
|
+
message: {
|
|
1110
|
+
headers: parsed.headerLines.map(header => libmime.decodeHeader(header.line)),
|
|
1111
|
+
attachments: parsed.attachments,
|
|
1112
|
+
html: parsed.html,
|
|
1113
|
+
text: parsed.text
|
|
1114
|
+
},
|
|
1115
|
+
openAiAPIKey: request.payload.openAiAPIKey,
|
|
1116
|
+
openAiModel: request.payload.openAiModel,
|
|
1117
|
+
openAiAPIUrl: request.payload.openAiAPIUrl,
|
|
1118
|
+
openAiPrompt: request.payload.openAiPrompt,
|
|
1119
|
+
openAiTemperature: request.payload.openAiTemperature,
|
|
1120
|
+
openAiTopP: request.payload.openAiTopP,
|
|
1121
|
+
openAiMaxTokens: request.payload.openAiMaxTokens
|
|
1122
|
+
},
|
|
1123
|
+
timeout: 2 * 60 * 1000
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
for (let key of Object.keys(response.summary)) {
|
|
1127
|
+
if (key.charAt(0) === '_' || response.summary[key] === '') {
|
|
1128
|
+
delete response.summary[key];
|
|
1129
|
+
}
|
|
1130
|
+
if (key === 'riskAssessment') {
|
|
1131
|
+
response.riskAssessment = response.summary.riskAssessment;
|
|
1132
|
+
delete response.summary.riskAssessment;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return { success: true, response };
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
request.logger.error({ msg: 'Failed to test prompt', err });
|
|
1139
|
+
return { success: false, error: err.message };
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
options: {
|
|
1143
|
+
payload: { maxBytes: MAX_BODY_SIZE, timeout: MAX_PAYLOAD_TIMEOUT },
|
|
1144
|
+
validate: {
|
|
1145
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
1146
|
+
failAction,
|
|
1147
|
+
payload: Joi.object({
|
|
1148
|
+
emailFile: Joi.string().base64({ paddingRequired: false }).required(),
|
|
1149
|
+
openAiAPIKey: settingsSchema.openAiAPIKey.empty(''),
|
|
1150
|
+
openAiModel: settingsSchema.openAiModel.empty(''),
|
|
1151
|
+
openAiAPIUrl: settingsSchema.openAiAPIUrl.default(''),
|
|
1152
|
+
openAiPrompt: settingsSchema.openAiPrompt.default(''),
|
|
1153
|
+
openAiTemperature: settingsSchema.openAiTemperature.empty(''),
|
|
1154
|
+
openAiTopP: settingsSchema.openAiTopP.empty(''),
|
|
1155
|
+
openAiMaxTokens: settingsSchema.openAiMaxTokens.empty('')
|
|
1156
|
+
})
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
// AI reload models route
|
|
1162
|
+
server.route({
|
|
1163
|
+
method: 'POST',
|
|
1164
|
+
path: '/admin/config/ai/reload-models',
|
|
1165
|
+
async handler(request) {
|
|
1166
|
+
try {
|
|
1167
|
+
request.logger.info({ msg: 'Reload models' });
|
|
1168
|
+
|
|
1169
|
+
const { models } = await call({
|
|
1170
|
+
cmd: 'openAiListModels',
|
|
1171
|
+
data: {
|
|
1172
|
+
openAiAPIKey: request.payload.openAiAPIKey,
|
|
1173
|
+
openAiAPIUrl: request.payload.openAiAPIUrl
|
|
1174
|
+
},
|
|
1175
|
+
timeout: 2 * 60 * 1000
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
if (models && models.length) {
|
|
1179
|
+
await settings.set('openAiModels', models);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return { success: true, models };
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
request.logger.error({ msg: 'Failed reloading OpenAI models', err });
|
|
1185
|
+
return { success: false, error: err.message };
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1188
|
+
options: {
|
|
1189
|
+
validate: {
|
|
1190
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
1191
|
+
failAction,
|
|
1192
|
+
payload: Joi.object({
|
|
1193
|
+
openAiAPIKey: settingsSchema.openAiAPIKey.empty(''),
|
|
1194
|
+
openAiAPIUrl: settingsSchema.openAiAPIUrl.default('')
|
|
1195
|
+
})
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// Browser config route
|
|
1201
|
+
server.route({
|
|
1202
|
+
method: 'POST',
|
|
1203
|
+
path: '/admin/config/browser',
|
|
1204
|
+
async handler(request) {
|
|
1205
|
+
for (let key of ['serviceUrl', 'language', 'timezone']) {
|
|
1206
|
+
if (request.payload[key]) {
|
|
1207
|
+
let existingValue = await settings.get(key);
|
|
1208
|
+
if (existingValue === null) {
|
|
1209
|
+
await settings.set(key, request.payload[key]);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return { success: true };
|
|
1214
|
+
},
|
|
1215
|
+
options: {
|
|
1216
|
+
validate: {
|
|
1217
|
+
options: { stripUnknown: true, abortEarly: false, convert: true },
|
|
1218
|
+
failAction,
|
|
1219
|
+
payload: Joi.object({
|
|
1220
|
+
serviceUrl: settingsSchema.serviceUrl.empty('').allow(false),
|
|
1221
|
+
language: Joi.string()
|
|
1222
|
+
.empty('')
|
|
1223
|
+
.lowercase()
|
|
1224
|
+
.regex(/^[a-z0-9]{1,5}([-_][a-z0-9]{1,15})?$/)
|
|
1225
|
+
.allow(false),
|
|
1226
|
+
timezone: Joi.string().empty('').allow(false).max(255)
|
|
1227
|
+
})
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
module.exports = init;
|