shutro-erp 0.3.4 → 0.3.5
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/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +54 -3
- package/dist/account_portal/module.d.ts +359 -6
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +54 -3
- package/dist/accounting/index.js +214 -2
- package/dist/accounting/module.d.ts +217 -5
- package/dist/accounting/module.js +23 -2
- package/dist/calendar/.kosh/bindings.d.ts +22 -0
- package/dist/calendar/.kosh/bindings.js +15 -0
- package/dist/calendar/.kosh/schema.d.ts +795 -0
- package/dist/calendar/demo.d.ts +6 -0
- package/dist/calendar/demo.js +24 -0
- package/dist/calendar/index.d.ts +3 -0
- package/dist/calendar/index.js +291 -0
- package/dist/calendar/module.d.ts +1705 -0
- package/dist/calendar/module.js +180 -0
- package/dist/cli/init.js +48 -0
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +137 -0
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +78 -3
- package/dist/contacts/demo.js +30 -1
- package/dist/contacts/index.d.ts +1 -1
- package/dist/contacts/index.js +155 -9
- package/dist/contacts/module.d.ts +338 -2
- package/dist/contacts/module.js +82 -5
- package/dist/crm/.kosh/bindings.d.ts +22 -0
- package/dist/crm/.kosh/bindings.js +15 -0
- package/dist/crm/.kosh/schema.d.ts +799 -0
- package/dist/crm/demo.d.ts +6 -0
- package/dist/crm/demo.js +45 -0
- package/dist/crm/index.d.ts +3 -0
- package/dist/crm/index.js +361 -0
- package/dist/crm/module.d.ts +1843 -0
- package/dist/crm/module.js +207 -0
- package/dist/event/.kosh/bindings.d.ts +22 -0
- package/dist/event/.kosh/bindings.js +15 -0
- package/dist/event/.kosh/schema.d.ts +843 -0
- package/dist/event/demo.d.ts +6 -0
- package/dist/event/demo.js +65 -0
- package/dist/event/index.d.ts +14 -0
- package/dist/event/index.js +333 -0
- package/dist/event/mails.d.ts +15 -0
- package/dist/event/mails.js +112 -0
- package/dist/event/module.d.ts +2800 -0
- package/dist/event/module.js +323 -0
- package/dist/event/web/CheckInBox.d.ts +9 -0
- package/dist/event/web/CheckInBox.js +87 -0
- package/dist/event/web/index.d.ts +11 -0
- package/dist/event/web/index.js +16 -0
- package/dist/event/web/messages.d.ts +2 -0
- package/dist/event/web/messages.js +23 -0
- package/dist/event_account/.kosh/bindings.d.ts +22 -0
- package/dist/event_account/.kosh/bindings.js +15 -0
- package/dist/event_account/.kosh/schema.d.ts +1639 -0
- package/dist/event_account/index.d.ts +3 -0
- package/dist/event_account/index.js +28 -0
- package/dist/event_account/module.d.ts +6968 -0
- package/dist/event_account/module.js +90 -0
- package/dist/hr/.kosh/bindings.d.ts +22 -0
- package/dist/hr/.kosh/bindings.js +15 -0
- package/dist/hr/.kosh/schema.d.ts +878 -0
- package/dist/hr/demo.d.ts +6 -0
- package/dist/hr/demo.js +60 -0
- package/dist/hr/index.d.ts +3 -0
- package/dist/hr/index.js +483 -0
- package/dist/hr/module.d.ts +2611 -0
- package/dist/hr/module.js +350 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +54 -3
- package/dist/l10n_bd/module.d.ts +359 -6
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +54 -3
- package/dist/l10n_in/module.d.ts +380 -7
- package/dist/maintenance/.kosh/bindings.d.ts +22 -0
- package/dist/maintenance/.kosh/bindings.js +15 -0
- package/dist/maintenance/.kosh/schema.d.ts +911 -0
- package/dist/maintenance/demo.d.ts +6 -0
- package/dist/maintenance/demo.js +60 -0
- package/dist/maintenance/index.d.ts +3 -0
- package/dist/maintenance/index.js +422 -0
- package/dist/maintenance/module.d.ts +3545 -0
- package/dist/maintenance/module.js +446 -0
- package/dist/maintenance_stock/.kosh/bindings.d.ts +22 -0
- package/dist/maintenance_stock/.kosh/bindings.js +15 -0
- package/dist/maintenance_stock/.kosh/schema.d.ts +1371 -0
- package/dist/maintenance_stock/index.d.ts +9 -0
- package/dist/maintenance_stock/index.js +52 -0
- package/dist/maintenance_stock/module.d.ts +6953 -0
- package/dist/maintenance_stock/module.js +192 -0
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/manifest.d.ts +70 -0
- package/dist/modules-sdk/manifest.js +35 -1
- package/dist/mrp/.kosh/bindings.d.ts +22 -0
- package/dist/mrp/.kosh/bindings.js +15 -0
- package/dist/mrp/.kosh/schema.d.ts +1378 -0
- package/dist/mrp/demo.d.ts +7 -0
- package/dist/mrp/demo.js +42 -0
- package/dist/mrp/index.d.ts +3 -0
- package/dist/mrp/index.js +392 -0
- package/dist/mrp/module.d.ts +8608 -0
- package/dist/mrp/module.js +695 -0
- package/dist/mrp_account/.kosh/bindings.d.ts +22 -0
- package/dist/mrp_account/.kosh/bindings.js +15 -0
- package/dist/mrp_account/.kosh/schema.d.ts +2106 -0
- package/dist/mrp_account/index.d.ts +3 -0
- package/dist/mrp_account/index.js +22 -0
- package/dist/mrp_account/module.d.ts +20840 -0
- package/dist/mrp_account/module.js +123 -0
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +54 -3
- package/dist/payment/module.d.ts +359 -6
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +78 -3
- package/dist/portal/module.d.ts +166 -2
- package/dist/pos/.kosh/bindings.d.ts +22 -0
- package/dist/pos/.kosh/bindings.js +15 -0
- package/dist/pos/.kosh/schema.d.ts +1690 -0
- package/dist/pos/accounting.d.ts +10 -0
- package/dist/pos/accounting.js +149 -0
- package/dist/pos/demo.d.ts +6 -0
- package/dist/pos/demo.js +90 -0
- package/dist/pos/index.d.ts +9 -0
- package/dist/pos/index.js +349 -0
- package/dist/pos/module.d.ts +9636 -0
- package/dist/pos/module.js +423 -0
- package/dist/pos/routes.d.ts +39 -0
- package/dist/pos/routes.js +368 -0
- package/dist/pos/web/Register.d.ts +9 -0
- package/dist/pos/web/Register.js +252 -0
- package/dist/pos/web/index.d.ts +11 -0
- package/dist/pos/web/index.js +16 -0
- package/dist/pos/web/messages.d.ts +2 -0
- package/dist/pos/web/messages.js +87 -0
- package/dist/pos_stock/.kosh/bindings.d.ts +22 -0
- package/dist/pos_stock/.kosh/bindings.js +15 -0
- package/dist/pos_stock/.kosh/schema.d.ts +2015 -0
- package/dist/pos_stock/index.d.ts +9 -0
- package/dist/pos_stock/index.js +35 -0
- package/dist/pos_stock/module.d.ts +11935 -0
- package/dist/pos_stock/module.js +115 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +51 -1
- package/dist/product/index.js +81 -4
- package/dist/product/module.d.ts +43 -1
- package/dist/product/module.js +9 -2
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +54 -3
- package/dist/project/index.js +147 -7
- package/dist/project/module.d.ts +210 -2
- package/dist/project/module.js +52 -5
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +54 -3
- package/dist/project_portal/module.d.ts +353 -4
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +54 -3
- package/dist/purchase/index.js +163 -3
- package/dist/purchase/module.d.ts +215 -3
- package/dist/purchase/module.js +21 -3
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +55 -3
- package/dist/purchase_account/index.js +26 -1
- package/dist/purchase_account/module.d.ts +393 -7
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +54 -3
- package/dist/purchase_portal/module.d.ts +366 -5
- package/dist/purchase_stock/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_stock/.kosh/bindings.js +15 -0
- package/dist/purchase_stock/.kosh/schema.d.ts +1310 -0
- package/dist/purchase_stock/index.d.ts +7 -0
- package/dist/purchase_stock/index.js +91 -0
- package/dist/purchase_stock/module.d.ts +6763 -0
- package/dist/purchase_stock/module.js +144 -0
- package/dist/purchase_stock_account/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_stock_account/.kosh/bindings.js +15 -0
- package/dist/purchase_stock_account/.kosh/schema.d.ts +1959 -0
- package/dist/purchase_stock_account/index.d.ts +3 -0
- package/dist/purchase_stock_account/index.js +24 -0
- package/dist/purchase_stock_account/module.d.ts +35366 -0
- package/dist/purchase_stock_account/module.js +162 -0
- package/dist/recruitment/.kosh/bindings.d.ts +22 -0
- package/dist/recruitment/.kosh/bindings.js +15 -0
- package/dist/recruitment/.kosh/schema.d.ts +990 -0
- package/dist/recruitment/demo.d.ts +6 -0
- package/dist/recruitment/demo.js +45 -0
- package/dist/recruitment/index.d.ts +3 -0
- package/dist/recruitment/index.js +385 -0
- package/dist/recruitment/module.d.ts +3247 -0
- package/dist/recruitment/module.js +193 -0
- package/dist/recruitment/web/HireButton.d.ts +9 -0
- package/dist/recruitment/web/HireButton.js +32 -0
- package/dist/recruitment/web/index.d.ts +11 -0
- package/dist/recruitment/web/index.js +16 -0
- package/dist/recruitment/web/messages.d.ts +2 -0
- package/dist/recruitment/web/messages.js +19 -0
- package/dist/reporting/documents.d.ts +5 -0
- package/dist/reporting/documents.js +28 -1
- package/dist/reporting/index.d.ts +1 -1
- package/dist/reporting/index.js +4 -2
- package/dist/reporting/models.js +9 -0
- package/dist/reporting/overviews.d.ts +16 -0
- package/dist/reporting/overviews.js +56 -0
- package/dist/reporting/routes-documents.js +1 -1
- package/dist/reporting/routes.js +138 -80
- package/dist/reporting/web/DashboardView.d.ts +11 -0
- package/dist/reporting/web/DashboardView.js +2 -2
- package/dist/reporting/web/DesignerView.js +5 -0
- package/dist/reporting/web/Overview.d.ts +9 -0
- package/dist/reporting/web/Overview.js +53 -0
- package/dist/reporting/web/index.d.ts +1 -0
- package/dist/reporting/web/index.js +4 -1
- package/dist/reporting/web/messages.js +9 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +59 -3
- package/dist/sale_account/index.js +22 -1
- package/dist/sale_account/module.d.ts +461 -7
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +58 -3
- package/dist/sale_portal/module.d.ts +434 -5
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +60 -3
- package/dist/sale_project/module.d.ts +476 -6
- package/dist/sale_stock/.kosh/bindings.d.ts +22 -0
- package/dist/sale_stock/.kosh/bindings.js +15 -0
- package/dist/sale_stock/.kosh/schema.d.ts +1228 -0
- package/dist/sale_stock/index.d.ts +7 -0
- package/dist/sale_stock/index.js +84 -0
- package/dist/sale_stock/module.d.ts +6791 -0
- package/dist/sale_stock/module.js +146 -0
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +58 -3
- package/dist/sales/index.js +191 -4
- package/dist/sales/module.d.ts +355 -3
- package/dist/sales/module.js +78 -3
- package/dist/server/api/metadata.d.ts +5 -0
- package/dist/server/api/metadata.js +5 -0
- package/dist/server/api/rest.js +64 -1
- package/dist/server/api/views.js +1 -0
- package/dist/server/app-settings.d.ts +7 -0
- package/dist/server/app-settings.js +10 -0
- package/dist/server/auth/base.js +9 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/settings/app-settings.d.ts +2 -0
- package/dist/settings/app-settings.js +518 -0
- package/dist/settings/index.d.ts +1 -0
- package/dist/settings/index.js +15 -0
- package/dist/settings/people.js +29 -5
- package/dist/settings/web/AppSettingsPage.d.ts +11 -0
- package/dist/settings/web/AppSettingsPage.js +210 -0
- package/dist/settings/web/OverviewPage.js +16 -3
- package/dist/settings/web/PeoplePage.js +20 -6
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +3 -0
- package/dist/settings/web/messages.js +118 -2
- package/dist/stock/.kosh/bindings.d.ts +22 -0
- package/dist/stock/.kosh/bindings.js +15 -0
- package/dist/stock/.kosh/schema.d.ts +1147 -0
- package/dist/stock/demo.d.ts +7 -0
- package/dist/stock/demo.js +55 -0
- package/dist/stock/index.d.ts +3 -0
- package/dist/stock/index.js +674 -0
- package/dist/stock/module.d.ts +6330 -0
- package/dist/stock/module.js +1325 -0
- package/dist/stock/web/ScanBox.d.ts +8 -0
- package/dist/stock/web/ScanBox.js +90 -0
- package/dist/stock/web/index.d.ts +11 -0
- package/dist/stock/web/index.js +16 -0
- package/dist/stock/web/messages.d.ts +2 -0
- package/dist/stock/web/messages.js +25 -0
- package/dist/stock_account/.kosh/bindings.d.ts +22 -0
- package/dist/stock_account/.kosh/bindings.js +15 -0
- package/dist/stock_account/.kosh/schema.d.ts +1875 -0
- package/dist/stock_account/index.d.ts +3 -0
- package/dist/stock_account/index.js +130 -0
- package/dist/stock_account/module.d.ts +10655 -0
- package/dist/stock_account/module.js +339 -0
- package/dist/storage/attachments.d.ts +5 -0
- package/dist/storage/attachments.js +12 -1
- package/dist/storage/models.js +3 -0
- package/dist/storage/routes.js +13 -2
- package/dist/todo/.kosh/bindings.d.ts +22 -0
- package/dist/todo/.kosh/bindings.js +15 -0
- package/dist/todo/.kosh/schema.d.ts +732 -0
- package/dist/todo/demo.d.ts +6 -0
- package/dist/todo/demo.js +35 -0
- package/dist/todo/index.d.ts +3 -0
- package/dist/todo/index.js +187 -0
- package/dist/todo/module.d.ts +1180 -0
- package/dist/todo/module.js +130 -0
- package/dist/trade_finance/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance/.kosh/bindings.js +15 -0
- package/dist/trade_finance/.kosh/schema.d.ts +884 -0
- package/dist/trade_finance/demo.d.ts +6 -0
- package/dist/trade_finance/demo.js +63 -0
- package/dist/trade_finance/index.d.ts +3 -0
- package/dist/trade_finance/index.js +413 -0
- package/dist/trade_finance/module.d.ts +3779 -0
- package/dist/trade_finance/module.js +505 -0
- package/dist/trade_finance_account/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_account/.kosh/bindings.js +15 -0
- package/dist/trade_finance_account/.kosh/schema.d.ts +1711 -0
- package/dist/trade_finance_account/index.d.ts +3 -0
- package/dist/trade_finance_account/index.js +92 -0
- package/dist/trade_finance_account/module.d.ts +7928 -0
- package/dist/trade_finance_account/module.js +244 -0
- package/dist/trade_finance_purchase/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_purchase/.kosh/bindings.js +15 -0
- package/dist/trade_finance_purchase/.kosh/schema.d.ts +1153 -0
- package/dist/trade_finance_purchase/index.d.ts +8 -0
- package/dist/trade_finance_purchase/index.js +50 -0
- package/dist/trade_finance_purchase/module.d.ts +4252 -0
- package/dist/trade_finance_purchase/module.js +44 -0
- package/dist/trade_finance_sale/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_sale/.kosh/bindings.js +15 -0
- package/dist/trade_finance_sale/.kosh/schema.d.ts +1071 -0
- package/dist/trade_finance_sale/index.d.ts +8 -0
- package/dist/trade_finance_sale/index.js +50 -0
- package/dist/trade_finance_sale/module.d.ts +4280 -0
- package/dist/trade_finance_sale/module.js +44 -0
- package/dist/ui/api.d.ts +2 -1
- package/dist/ui/api.js +2 -1
- package/dist/ui/app.js +2 -1
- package/dist/ui/components/AppIcon.js +23 -1
- package/dist/ui/components/primitives.d.ts +2 -0
- package/dist/ui/components/primitives.js +7 -1
- package/dist/ui/i18n/messages.d.ts +23 -0
- package/dist/ui/i18n/messages.js +38 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/offline/queue.d.ts +5 -1
- package/dist/ui/offline/queue.js +2 -0
- package/dist/ui/registry.d.ts +5 -0
- package/dist/ui/shell/Home.js +55 -4
- package/dist/ui/views/FormView.js +98 -17
- package/dist/ui/views/KanbanView.d.ts +2 -0
- package/dist/ui/views/KanbanView.js +32 -3
- package/dist/ui/views/ListView.js +108 -11
- package/dist/ui/views/StatsRow.d.ts +14 -0
- package/dist/ui/views/StatsRow.js +88 -0
- package/dist/ui/views/widgets.d.ts +2 -1
- package/dist/ui/views/widgets.js +194 -1
- package/dist/views/builders.d.ts +19 -2
- package/dist/views/builders.js +21 -1
- package/dist/views/condition.d.ts +6 -0
- package/dist/views/condition.js +20 -0
- package/dist/views/documents.d.ts +8 -0
- package/dist/views/extend.d.ts +2 -2
- package/dist/views/extend.js +11 -1
- package/dist/views/index.d.ts +2 -2
- package/dist/views/index.js +2 -2
- package/dist/views/types.d.ts +52 -5
- package/dist/views/validate.js +39 -1
- package/dist/website/.kosh/bindings.d.ts +22 -0
- package/dist/website/.kosh/bindings.js +15 -0
- package/dist/website/.kosh/schema.d.ts +881 -0
- package/dist/website/demo.d.ts +6 -0
- package/dist/website/demo.js +115 -0
- package/dist/website/index.d.ts +9 -0
- package/dist/website/index.js +250 -0
- package/dist/website/module.d.ts +2987 -0
- package/dist/website/module.js +281 -0
- package/dist/website/render.d.ts +45 -0
- package/dist/website/render.js +102 -0
- package/dist/website/routes.d.ts +12 -0
- package/dist/website/routes.js +246 -0
- package/dist/website/web/PagePreview.d.ts +7 -0
- package/dist/website/web/PagePreview.js +33 -0
- package/dist/website/web/index.d.ts +11 -0
- package/dist/website/web/index.js +16 -0
- package/dist/website/web/messages.d.ts +2 -0
- package/dist/website/web/messages.js +15 -0
- package/dist/website_crm/.kosh/bindings.d.ts +22 -0
- package/dist/website_crm/.kosh/bindings.js +15 -0
- package/dist/website_crm/.kosh/schema.d.ts +966 -0
- package/dist/website_crm/index.d.ts +3 -0
- package/dist/website_crm/index.js +39 -0
- package/dist/website_crm/module.d.ts +2955 -0
- package/dist/website_crm/module.js +44 -0
- package/package.json +118 -1
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { audit, HttpProblem, publicApiModels, ServerConfigError, } from '../server/index.js';
|
|
2
|
+
const ref = (env, name) => env.model(name);
|
|
3
|
+
const TYPES = new Set(['boolean', 'integer', 'decimal', 'char', 'text', 'selection', 'date']);
|
|
4
|
+
/** Company field kinds a setting may be. */
|
|
5
|
+
const KINDS = {
|
|
6
|
+
boolean: 'boolean',
|
|
7
|
+
integer: 'integer',
|
|
8
|
+
decimal: 'decimal',
|
|
9
|
+
monetary: 'decimal',
|
|
10
|
+
float: 'decimal',
|
|
11
|
+
char: 'char',
|
|
12
|
+
text: 'text',
|
|
13
|
+
selection: 'selection',
|
|
14
|
+
date: 'date',
|
|
15
|
+
};
|
|
16
|
+
export function registerAppSettingsRoutes(context) {
|
|
17
|
+
const { app, registry } = context;
|
|
18
|
+
const modules = registry.loaded.modules;
|
|
19
|
+
const company = publicApiModels(registry).get('res.company');
|
|
20
|
+
const companyFields = new Map((company?.fields ?? []).map((f) => [f.key, f]));
|
|
21
|
+
const descriptors = registry.registry.get('res.company').model.fields;
|
|
22
|
+
const groups = registry.registry.security.groups;
|
|
23
|
+
// ---- the declarations, checked once at start: a wrong one stops the server, like other manifest errors.
|
|
24
|
+
const fail = (module, detail) => new ServerConfigError(`The settings of ${module}: ${detail}`, 'Fix the module’s settings declaration.');
|
|
25
|
+
const fromField = (module, spec, field) => {
|
|
26
|
+
if (field === undefined || !field.stored)
|
|
27
|
+
throw fail(module, `'${spec.key}' is not a stored res.company field (add it with extendModel).`);
|
|
28
|
+
const type = KINDS[field.kind];
|
|
29
|
+
if (type === undefined)
|
|
30
|
+
throw fail(module, `'${spec.key}' is a ${field.kind} field: not a setting type.`);
|
|
31
|
+
const raw = descriptors[spec.key]?.['default'];
|
|
32
|
+
return {
|
|
33
|
+
key: spec.key,
|
|
34
|
+
type,
|
|
35
|
+
label: spec.label ?? field.label,
|
|
36
|
+
help: spec.help ?? null,
|
|
37
|
+
default: spec.default !== undefined
|
|
38
|
+
? spec.default
|
|
39
|
+
: typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean'
|
|
40
|
+
? raw
|
|
41
|
+
: null,
|
|
42
|
+
min: spec.min ?? null,
|
|
43
|
+
max: spec.max ?? null,
|
|
44
|
+
options: field.options === null ? null : field.options.map(([value, label]) => ({ value, label })),
|
|
45
|
+
visibleWhen: spec.visibleWhen ?? null,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const ownSetting = (module, spec) => {
|
|
49
|
+
if (spec.type === undefined || !TYPES.has(spec.type) || spec.label === undefined)
|
|
50
|
+
throw fail(module, `'${spec.key}' needs a type and a label (an installation setting).`);
|
|
51
|
+
if (spec.type === 'selection' && spec.options === undefined)
|
|
52
|
+
throw fail(module, `'${spec.key}' is a selection without options.`);
|
|
53
|
+
return {
|
|
54
|
+
key: spec.key,
|
|
55
|
+
type: spec.type,
|
|
56
|
+
label: spec.label,
|
|
57
|
+
help: spec.help ?? null,
|
|
58
|
+
default: spec.default ?? null,
|
|
59
|
+
min: spec.min ?? null,
|
|
60
|
+
max: spec.max ?? null,
|
|
61
|
+
options: spec.options === undefined
|
|
62
|
+
? null
|
|
63
|
+
: Object.entries(spec.options).map(([value, label]) => ({ value, label })),
|
|
64
|
+
visibleWhen: spec.visibleWhen ?? null,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const sections = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const m of modules)
|
|
70
|
+
for (const spec of m.settings?.sections ?? []) {
|
|
71
|
+
if (!/^[a-z][a-z0-9_]*$/.test(spec.key))
|
|
72
|
+
throw fail(m.name, `the section key '${spec.key}'.`);
|
|
73
|
+
if (seen.has(`${m.name}.${spec.key}`))
|
|
74
|
+
throw fail(m.name, `two sections '${spec.key}'.`);
|
|
75
|
+
seen.add(`${m.name}.${spec.key}`);
|
|
76
|
+
for (const g of spec.editors ?? [])
|
|
77
|
+
if (!groups.has(g))
|
|
78
|
+
throw fail(m.name, `the editor group '${g}' is not defined.`);
|
|
79
|
+
const keys = new Set();
|
|
80
|
+
const settings = spec.fields.map((f) => {
|
|
81
|
+
if (keys.has(f.key))
|
|
82
|
+
throw fail(m.name, `'${f.key}' twice in '${spec.key}'.`);
|
|
83
|
+
keys.add(f.key);
|
|
84
|
+
return spec.scope === 'company'
|
|
85
|
+
? fromField(m.name, f, companyFields.get(f.key))
|
|
86
|
+
: ownSetting(m.name, f);
|
|
87
|
+
});
|
|
88
|
+
for (const s of settings)
|
|
89
|
+
if (s.visibleWhen !== null && !keys.has(s.visibleWhen.key))
|
|
90
|
+
throw fail(m.name, `'${s.key}' depends on '${s.visibleWhen.key}', not in its section.`);
|
|
91
|
+
sections.push({ app: m.name, spec, settings });
|
|
92
|
+
}
|
|
93
|
+
/** The groups that open an app: its access levels' groups. */
|
|
94
|
+
const openers = new Map(modules.map((m) => [
|
|
95
|
+
m.name,
|
|
96
|
+
new Set(Object.values(m.accessLevels ?? {}).flatMap((l) => [...l.groups])),
|
|
97
|
+
]));
|
|
98
|
+
const mayView = (env, module) => env.administrator || [...(openers.get(module) ?? [])].some((g) => env.groups.has(g));
|
|
99
|
+
const mayEdit = (env, section) => env.administrator || (section.spec.editors ?? []).some((g) => env.groups.has(g));
|
|
100
|
+
const label = (module) => {
|
|
101
|
+
const m = modules.find((x) => x.name === module);
|
|
102
|
+
return { label: m?.app?.label ?? module, icon: m?.app?.icon ?? null };
|
|
103
|
+
};
|
|
104
|
+
const storedKey = (section, key) => `${section.app}.${key}`;
|
|
105
|
+
/** The current values of a section: the company's fields, or the installation's records. */
|
|
106
|
+
const valuesOf = async (su, section, companyId) => {
|
|
107
|
+
if (section.spec.scope === 'company') {
|
|
108
|
+
const keys = section.settings.map((s) => s.key);
|
|
109
|
+
const [row] = (await ref(su, 'res.company').browse([companyId]).load(keys)).read(keys);
|
|
110
|
+
return Object.fromEntries(keys.map((k) => [k, normalize(row?.[k])]));
|
|
111
|
+
}
|
|
112
|
+
const found = await ref(su, 'kosh.setting').search({
|
|
113
|
+
where: { key: { in: section.settings.map((s) => storedKey(section, s.key)) } },
|
|
114
|
+
});
|
|
115
|
+
const rows = found.length === 0 ? [] : (await found.load(['key', 'value'])).read(['key', 'value']);
|
|
116
|
+
const stored = new Map(rows.map((r) => [String(r['key']), r['value']]));
|
|
117
|
+
return Object.fromEntries(section.settings.map((s) => [
|
|
118
|
+
s.key,
|
|
119
|
+
stored.has(storedKey(section, s.key))
|
|
120
|
+
? stored.get(storedKey(section, s.key))
|
|
121
|
+
: s.default,
|
|
122
|
+
]));
|
|
123
|
+
};
|
|
124
|
+
/** A value checked against its setting. @throws {HttpProblem} 422 */
|
|
125
|
+
const check = (s, value) => {
|
|
126
|
+
const bad = (detail) => new HttpProblem(422, 'invalid-setting', 'Invalid setting', `${s.label}: ${detail}`);
|
|
127
|
+
if (value === null || value === '') {
|
|
128
|
+
if (s.type === 'boolean')
|
|
129
|
+
throw bad('yes or no.');
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
switch (s.type) {
|
|
133
|
+
case 'boolean':
|
|
134
|
+
if (typeof value !== 'boolean')
|
|
135
|
+
throw bad('yes or no.');
|
|
136
|
+
return value;
|
|
137
|
+
case 'integer': {
|
|
138
|
+
if (!Number.isSafeInteger(value))
|
|
139
|
+
throw bad('a whole number.');
|
|
140
|
+
const n = value;
|
|
141
|
+
if ((s.min !== null && n < s.min) || (s.max !== null && n > s.max))
|
|
142
|
+
throw bad(`from ${s.min ?? '…'} to ${s.max ?? '…'}.`);
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
case 'decimal': {
|
|
146
|
+
// Money and quantities stay strings (Shutro Decimal), never JS numbers.
|
|
147
|
+
const text = typeof value === 'number' ? String(value) : String(value);
|
|
148
|
+
if (!/^-?\d+(\.\d+)?$/.test(text))
|
|
149
|
+
throw bad('a number.');
|
|
150
|
+
return text;
|
|
151
|
+
}
|
|
152
|
+
case 'selection':
|
|
153
|
+
if (typeof value !== 'string' || !(s.options ?? []).some((o) => o.value === value))
|
|
154
|
+
throw bad('one of the choices.');
|
|
155
|
+
return value;
|
|
156
|
+
case 'date':
|
|
157
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
158
|
+
throw bad('a date (YYYY-MM-DD).');
|
|
159
|
+
return value;
|
|
160
|
+
default:
|
|
161
|
+
if (typeof value !== 'string' || value.length > (s.type === 'char' ? 500 : 20_000))
|
|
162
|
+
throw bad('text.');
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const sectionOf = (request) => {
|
|
167
|
+
const { app: module, section } = request.params;
|
|
168
|
+
const found = sections.find((s) => s.app === module && s.spec.key === section);
|
|
169
|
+
if (found === undefined)
|
|
170
|
+
throw new HttpProblem(404, 'missing-record', 'Not found', `There are no settings ${module}/${section}.`);
|
|
171
|
+
return found;
|
|
172
|
+
};
|
|
173
|
+
const shape = (section, values, editable) => ({
|
|
174
|
+
key: section.spec.key,
|
|
175
|
+
label: section.spec.label,
|
|
176
|
+
description: section.spec.description ?? null,
|
|
177
|
+
scope: section.spec.scope,
|
|
178
|
+
editable,
|
|
179
|
+
settings: section.settings.map((s) => ({
|
|
180
|
+
...s,
|
|
181
|
+
value: values[s.key] ?? null,
|
|
182
|
+
changed: (values[s.key] ?? null) !== s.default,
|
|
183
|
+
})),
|
|
184
|
+
});
|
|
185
|
+
app.get('/api/v1/settings/app-settings', async (request) => request.env(async (env) => {
|
|
186
|
+
const apps = [...new Set(sections.map((s) => s.app))].filter((m) => mayView(env, m));
|
|
187
|
+
return {
|
|
188
|
+
administrator: env.administrator,
|
|
189
|
+
apps: apps.map((m) => ({
|
|
190
|
+
app: m,
|
|
191
|
+
...label(m),
|
|
192
|
+
sections: sections
|
|
193
|
+
.filter((s) => s.app === m)
|
|
194
|
+
.map((s) => ({
|
|
195
|
+
key: s.spec.key,
|
|
196
|
+
label: s.spec.label,
|
|
197
|
+
description: s.spec.description ?? null,
|
|
198
|
+
keywords: [s.spec.keywords ?? '', ...s.settings.map((x) => x.label)].join(' '),
|
|
199
|
+
})),
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
}));
|
|
203
|
+
app.get('/api/v1/settings/app-settings/:app', async (request) => {
|
|
204
|
+
const module = request.params.app ?? '';
|
|
205
|
+
return request.env(async (env) => {
|
|
206
|
+
const mine = sections.filter((s) => s.app === module);
|
|
207
|
+
if (mine.length === 0 || !mayView(env, module))
|
|
208
|
+
throw new HttpProblem(404, 'missing-record', 'Not found', `There are no settings for ${module}.`);
|
|
209
|
+
return env.asSuperuser({ reason: 'app settings' }, async (su) => ({
|
|
210
|
+
app: module,
|
|
211
|
+
...label(module),
|
|
212
|
+
company: env.companyId,
|
|
213
|
+
sections: await Promise.all(mine.map(async (s) => shape(s, await valuesOf(su, s, env.companyId), mayEdit(env, s)))),
|
|
214
|
+
}));
|
|
215
|
+
}, { write: true });
|
|
216
|
+
});
|
|
217
|
+
/** Writes `changes` to the section's store for `companyId` and audits them with the old and new values. */
|
|
218
|
+
const apply = async (env, su, section, changes, companyId, how, extra, ip) => {
|
|
219
|
+
if (section.spec.scope === 'company')
|
|
220
|
+
await ref(su, 'res.company')
|
|
221
|
+
.browse([companyId])
|
|
222
|
+
.write(Object.fromEntries(Object.entries(changes).map(([k, c]) => [k, c.to])));
|
|
223
|
+
else
|
|
224
|
+
for (const [k, c] of Object.entries(changes)) {
|
|
225
|
+
const key = storedKey(section, k);
|
|
226
|
+
const existing = await ref(su, 'kosh.setting').search({ where: { key } });
|
|
227
|
+
if (existing.length > 0)
|
|
228
|
+
await existing.write({ value: c.to });
|
|
229
|
+
else
|
|
230
|
+
await ref(su, 'kosh.setting').create([{ key, value: c.to }]);
|
|
231
|
+
}
|
|
232
|
+
await audit(env, {
|
|
233
|
+
kind: 'settings.changed',
|
|
234
|
+
summary: `${label(section.app).label} › ${section.spec.label}: ${Object.keys(changes).join(', ')} ${how}.`,
|
|
235
|
+
details: {
|
|
236
|
+
app: section.app,
|
|
237
|
+
section: section.spec.key,
|
|
238
|
+
scope: section.spec.scope,
|
|
239
|
+
changes,
|
|
240
|
+
...extra,
|
|
241
|
+
},
|
|
242
|
+
company: companyId,
|
|
243
|
+
ip,
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
/** Writes `next` (only the keys that changed), audited with the old and new values. */
|
|
247
|
+
const save = (request, section, next) => request.env(async (env) => {
|
|
248
|
+
if (!mayView(env, section.app) || !mayEdit(env, section))
|
|
249
|
+
throw new HttpProblem(403, 'access', 'App settings', `Changing ${section.spec.label} needs ${(section.spec.editors ?? []).length === 0
|
|
250
|
+
? 'the Administrator role'
|
|
251
|
+
: `one of: ${(section.spec.editors ?? []).join(', ')}`}.`);
|
|
252
|
+
return env.asSuperuser({ reason: 'app settings' }, async (su) => {
|
|
253
|
+
const old = await valuesOf(su, section, env.companyId);
|
|
254
|
+
const wanted = next(old);
|
|
255
|
+
const changes = Object.fromEntries(Object.entries(wanted)
|
|
256
|
+
.filter(([k, v]) => old[k] !== v)
|
|
257
|
+
.map(([k, v]) => [k, { from: old[k] ?? null, to: v }]));
|
|
258
|
+
if (Object.keys(changes).length > 0) {
|
|
259
|
+
await apply(env, su, section, changes, env.companyId, 'changed', {}, request.ip);
|
|
260
|
+
await su.flush();
|
|
261
|
+
}
|
|
262
|
+
return shape(section, { ...old, ...wanted }, true);
|
|
263
|
+
});
|
|
264
|
+
}, { write: true });
|
|
265
|
+
app.patch('/api/v1/settings/app-settings/:app/:section', async (request) => {
|
|
266
|
+
const section = sectionOf(request);
|
|
267
|
+
const body = (request.body ?? {});
|
|
268
|
+
const values = body['values'];
|
|
269
|
+
if (typeof values !== 'object' || values === null || Array.isArray(values))
|
|
270
|
+
throw new HttpProblem(422, 'invalid-setting', 'Invalid setting', 'values is { key: value }.');
|
|
271
|
+
const checked = {};
|
|
272
|
+
for (const [key, value] of Object.entries(values)) {
|
|
273
|
+
const s = section.settings.find((x) => x.key === key);
|
|
274
|
+
if (s === undefined)
|
|
275
|
+
throw new HttpProblem(422, 'invalid-setting', 'Invalid setting', `'${key}' is not in ${section.spec.label}.`);
|
|
276
|
+
checked[key] = check(s, value);
|
|
277
|
+
}
|
|
278
|
+
return save(request, section, () => checked);
|
|
279
|
+
});
|
|
280
|
+
app.post('/api/v1/settings/app-settings/:app/:section/reset', async (request) => {
|
|
281
|
+
const section = sectionOf(request);
|
|
282
|
+
const keys = request.body?.['keys'];
|
|
283
|
+
const chosen = Array.isArray(keys) ? keys.map(String) : section.settings.map((s) => s.key);
|
|
284
|
+
const defaults = {};
|
|
285
|
+
for (const key of chosen) {
|
|
286
|
+
const s = section.settings.find((x) => x.key === key);
|
|
287
|
+
if (s === undefined)
|
|
288
|
+
throw new HttpProblem(422, 'invalid-setting', 'Invalid setting', `'${key}' is not in ${section.spec.label}.`);
|
|
289
|
+
defaults[key] = s.default;
|
|
290
|
+
}
|
|
291
|
+
return save(request, section, () => defaults);
|
|
292
|
+
});
|
|
293
|
+
// ---- step 2: history, copy between companies, export and import.
|
|
294
|
+
const administrators = (env) => {
|
|
295
|
+
if (!env.administrator)
|
|
296
|
+
throw new HttpProblem(403, 'access', 'App settings', 'This needs the Administrator role.');
|
|
297
|
+
};
|
|
298
|
+
/** Whether `companyId` is one of the user's companies. */
|
|
299
|
+
const memberOf = async (su, uid, companyId) => (await ref(su, 'res.users').search({
|
|
300
|
+
where: { id: uid, companies: { any: { id: companyId } } },
|
|
301
|
+
})).length > 0;
|
|
302
|
+
const companyOf = async (su, id) => {
|
|
303
|
+
const [row] = (await ref(su, 'res.company').browse([id]).load(['name'])).read(['name']);
|
|
304
|
+
if (row === undefined)
|
|
305
|
+
throw new HttpProblem(404, 'missing-record', 'Not found', `There is no company ${id}.`);
|
|
306
|
+
return { id, name: String(row['name']) };
|
|
307
|
+
};
|
|
308
|
+
const sectionKey = (s) => `${s.app}.${s.spec.key}`;
|
|
309
|
+
const describeChanges = (s, changes) => ({
|
|
310
|
+
app: s.app,
|
|
311
|
+
appLabel: label(s.app).label,
|
|
312
|
+
section: s.spec.key,
|
|
313
|
+
label: s.spec.label,
|
|
314
|
+
scope: s.spec.scope,
|
|
315
|
+
changes: Object.entries(changes).map(([key, c]) => ({
|
|
316
|
+
key,
|
|
317
|
+
label: s.settings.find((x) => x.key === key)?.label ?? key,
|
|
318
|
+
from: c.from,
|
|
319
|
+
to: c.to,
|
|
320
|
+
})),
|
|
321
|
+
});
|
|
322
|
+
app.get('/api/v1/settings/app-settings/export', async (request) => request.env(async (env) => {
|
|
323
|
+
administrators(env);
|
|
324
|
+
return env.asSuperuser({ reason: 'app settings export' }, async (su) => {
|
|
325
|
+
const installation = {};
|
|
326
|
+
const companySettings = {};
|
|
327
|
+
for (const s of sections)
|
|
328
|
+
(s.spec.scope === 'company' ? companySettings : installation)[sectionKey(s)] =
|
|
329
|
+
await valuesOf(su, s, env.companyId);
|
|
330
|
+
await audit(env, {
|
|
331
|
+
kind: 'settings.exported',
|
|
332
|
+
summary: 'App settings exported.',
|
|
333
|
+
ip: request.ip,
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
kosh: 'app-settings',
|
|
337
|
+
version: 1,
|
|
338
|
+
exportedAt: new Date().toISOString(),
|
|
339
|
+
company: await companyOf(su, env.companyId),
|
|
340
|
+
installation,
|
|
341
|
+
companySettings,
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
}, { write: true }));
|
|
345
|
+
app.get('/api/v1/settings/app-settings/:app/:section/history', async (request) => {
|
|
346
|
+
const section = sectionOf(request);
|
|
347
|
+
const key = request.query.key;
|
|
348
|
+
return request.env(async (env) => {
|
|
349
|
+
if (!mayView(env, section.app))
|
|
350
|
+
throw new HttpProblem(404, 'missing-record', 'Not found', 'There are no such settings.');
|
|
351
|
+
return env.asSuperuser({ reason: 'app settings history' }, async (su) => {
|
|
352
|
+
const found = await ref(su, 'audit.event').search({
|
|
353
|
+
where: {
|
|
354
|
+
kind: 'settings.changed',
|
|
355
|
+
...(section.spec.scope === 'company' ? { company: env.companyId } : {}),
|
|
356
|
+
},
|
|
357
|
+
order: ['id desc'],
|
|
358
|
+
limit: 500,
|
|
359
|
+
});
|
|
360
|
+
const rows = found.length === 0
|
|
361
|
+
? []
|
|
362
|
+
: (await found.load(['actor', 'createdAt', 'details'])).read([
|
|
363
|
+
'actor',
|
|
364
|
+
'createdAt',
|
|
365
|
+
'details',
|
|
366
|
+
]);
|
|
367
|
+
const events = rows
|
|
368
|
+
.map((r) => {
|
|
369
|
+
const details = (r['details'] ?? {});
|
|
370
|
+
const actor = r['actor'];
|
|
371
|
+
return {
|
|
372
|
+
actor: typeof actor === 'object' && actor !== null ? actor.id : actor,
|
|
373
|
+
actorName: typeof actor === 'object' && actor !== null ? (actor.name ?? null) : null,
|
|
374
|
+
at: r['createdAt'] instanceof Date
|
|
375
|
+
? r['createdAt'].toISOString()
|
|
376
|
+
: String(r['createdAt']),
|
|
377
|
+
details,
|
|
378
|
+
};
|
|
379
|
+
})
|
|
380
|
+
.filter((e) => e.details.app === section.app &&
|
|
381
|
+
e.details.section === section.spec.key &&
|
|
382
|
+
(key === undefined || key in (e.details.changes ?? {})))
|
|
383
|
+
.slice(0, 50);
|
|
384
|
+
const unnamed = [
|
|
385
|
+
...new Set(events
|
|
386
|
+
.filter((e) => e.actor !== null && e.actorName === null)
|
|
387
|
+
.map((e) => e.actor)),
|
|
388
|
+
];
|
|
389
|
+
const users = ref(su, 'res.users').browse(unnamed);
|
|
390
|
+
const names = new Map(unnamed.length === 0
|
|
391
|
+
? []
|
|
392
|
+
: (await users.load(['name']))
|
|
393
|
+
.read(['name'])
|
|
394
|
+
.map((r, i) => [users.ids[i], String(r['name'])]));
|
|
395
|
+
return {
|
|
396
|
+
events: events.map((e) => ({
|
|
397
|
+
at: e.at,
|
|
398
|
+
actor: e.actor === null
|
|
399
|
+
? null
|
|
400
|
+
: { id: e.actor, name: e.actorName ?? names.get(e.actor) ?? String(e.actor) },
|
|
401
|
+
how: e.details.copiedFrom !== undefined
|
|
402
|
+
? 'copied'
|
|
403
|
+
: e.details.imported === true
|
|
404
|
+
? 'imported'
|
|
405
|
+
: 'changed',
|
|
406
|
+
changes: Object.entries(e.details.changes ?? {})
|
|
407
|
+
.filter(([k]) => key === undefined || k === key)
|
|
408
|
+
.map(([k, c]) => ({
|
|
409
|
+
key: k,
|
|
410
|
+
label: section.settings.find((x) => x.key === k)?.label ?? k,
|
|
411
|
+
from: c.from,
|
|
412
|
+
to: c.to,
|
|
413
|
+
})),
|
|
414
|
+
})),
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
}, { write: true });
|
|
418
|
+
});
|
|
419
|
+
app.post('/api/v1/settings/app-settings/copy', async (request) => {
|
|
420
|
+
const body = (request.body ?? {});
|
|
421
|
+
const from = Number(body['from']);
|
|
422
|
+
const to = Number(body['to']);
|
|
423
|
+
const preview = body['preview'] === true;
|
|
424
|
+
const chosen = Array.isArray(body['sections']) ? body['sections'].map(String) : null;
|
|
425
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from === to)
|
|
426
|
+
throw new HttpProblem(422, 'invalid-copy', 'Copy settings', 'Give two different companies.');
|
|
427
|
+
return request.env(async (env) => {
|
|
428
|
+
administrators(env);
|
|
429
|
+
return env.asSuperuser({ reason: 'app settings copy' }, async (su) => {
|
|
430
|
+
for (const c of [from, to])
|
|
431
|
+
if (!(await memberOf(su, env.uid, c)))
|
|
432
|
+
throw new HttpProblem(403, 'access', 'Copy settings', `Company ${c} is not one of yours.`);
|
|
433
|
+
const source = await companyOf(su, from);
|
|
434
|
+
const target = await companyOf(su, to);
|
|
435
|
+
const picked = sections.filter((s) => s.spec.scope === 'company' && (chosen === null || chosen.includes(sectionKey(s))));
|
|
436
|
+
const out = [];
|
|
437
|
+
for (const s of picked) {
|
|
438
|
+
const a = await valuesOf(su, s, from);
|
|
439
|
+
const b = await valuesOf(su, s, to);
|
|
440
|
+
const changes = Object.fromEntries(s.settings
|
|
441
|
+
.filter((x) => (a[x.key] ?? null) !== (b[x.key] ?? null))
|
|
442
|
+
.map((x) => [x.key, { from: b[x.key] ?? null, to: a[x.key] ?? null }]));
|
|
443
|
+
if (Object.keys(changes).length === 0)
|
|
444
|
+
continue;
|
|
445
|
+
out.push(describeChanges(s, changes));
|
|
446
|
+
if (!preview)
|
|
447
|
+
await apply(env, su, s, changes, to, `copied from ${source.name}`, { copiedFrom: from }, request.ip);
|
|
448
|
+
}
|
|
449
|
+
if (!preview)
|
|
450
|
+
await su.flush();
|
|
451
|
+
return { preview, from: source, to: target, sections: out };
|
|
452
|
+
});
|
|
453
|
+
}, { write: true });
|
|
454
|
+
});
|
|
455
|
+
app.post('/api/v1/settings/app-settings/import', async (request) => {
|
|
456
|
+
const body = (request.body ?? {});
|
|
457
|
+
const file = body['file'];
|
|
458
|
+
const preview = body['preview'] === true;
|
|
459
|
+
if (typeof file !== 'object' || file === null || file['kosh'] !== 'app-settings')
|
|
460
|
+
throw new HttpProblem(422, 'invalid-file', 'Import settings', 'Not an app settings export.');
|
|
461
|
+
const given = [
|
|
462
|
+
['installation', file['installation']],
|
|
463
|
+
['company', file['companySettings']],
|
|
464
|
+
];
|
|
465
|
+
return request.env(async (env) => {
|
|
466
|
+
administrators(env);
|
|
467
|
+
return env.asSuperuser({ reason: 'app settings import' }, async (su) => {
|
|
468
|
+
const skipped = [];
|
|
469
|
+
const plan = [];
|
|
470
|
+
for (const [scope, part] of given) {
|
|
471
|
+
if (typeof part !== 'object' || part === null)
|
|
472
|
+
continue;
|
|
473
|
+
for (const [k, values] of Object.entries(part)) {
|
|
474
|
+
const s = sections.find((x) => sectionKey(x) === k && x.spec.scope === scope);
|
|
475
|
+
if (s === undefined || typeof values !== 'object' || values === null) {
|
|
476
|
+
skipped.push(k);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const current = await valuesOf(su, s, env.companyId);
|
|
480
|
+
const changes = {};
|
|
481
|
+
for (const [key, value] of Object.entries(values)) {
|
|
482
|
+
const setting = s.settings.find((x) => x.key === key);
|
|
483
|
+
if (setting === undefined) {
|
|
484
|
+
skipped.push(`${k}.${key}`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const checked = check(setting, value);
|
|
488
|
+
if (checked !== (current[key] ?? null))
|
|
489
|
+
changes[key] = { from: current[key] ?? null, to: checked };
|
|
490
|
+
}
|
|
491
|
+
if (Object.keys(changes).length > 0)
|
|
492
|
+
plan.push({ section: s, changes });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (!preview) {
|
|
496
|
+
for (const { section, changes } of plan)
|
|
497
|
+
await apply(env, su, section, changes, env.companyId, 'imported', { imported: true }, request.ip);
|
|
498
|
+
await su.flush();
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
preview,
|
|
502
|
+
skipped,
|
|
503
|
+
sections: plan.map(({ section, changes }) => describeChanges(section, changes)),
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
}, { write: true });
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/** Database values as settings carry them: dates as YYYY-MM-DD, decimals as strings. */
|
|
510
|
+
function normalize(value) {
|
|
511
|
+
if (value === undefined || value === null)
|
|
512
|
+
return null;
|
|
513
|
+
if (value instanceof Date)
|
|
514
|
+
return value.toISOString().slice(0, 10);
|
|
515
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
516
|
+
return value;
|
|
517
|
+
return String(value);
|
|
518
|
+
}
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type KoshModule } from '../modules-sdk/index.js';
|
|
2
2
|
import { orm } from '../server/orm.js';
|
|
3
3
|
import { type PeopleOptions } from './people.js';
|
|
4
|
+
export { registerAppSettingsRoutes } from './app-settings.js';
|
|
4
5
|
export { type CompanyDetail, type CompanySettings, registerCompanyRoutes } from './company.js';
|
|
5
6
|
export { type Assignment, INVITATION_DAYS, type PeopleOptions, registerPeopleRoutes, } from './people.js';
|
|
6
7
|
export { type AppInfo, type PlanInfo, PROTECTED_APPS, registerAppRoutes } from './routes.js';
|
package/dist/settings/index.js
CHANGED
|
@@ -11,10 +11,12 @@ import { mailerFromEnv } from '../mail/index.js';
|
|
|
11
11
|
import { defineModule } from '../modules-sdk/index.js';
|
|
12
12
|
import { base, baseShutro } from '../server/index.js';
|
|
13
13
|
import { orm } from '../server/orm.js';
|
|
14
|
+
import { registerAppSettingsRoutes } from './app-settings.js';
|
|
14
15
|
import { registerCompanyRoutes } from './company.js';
|
|
15
16
|
import { registerPeopleRoutes } from './people.js';
|
|
16
17
|
import { registerAppRoutes } from './routes.js';
|
|
17
18
|
import { registerSecurityRoutes } from './security.js';
|
|
19
|
+
export { registerAppSettingsRoutes } from './app-settings.js';
|
|
18
20
|
export { registerCompanyRoutes } from './company.js';
|
|
19
21
|
export { INVITATION_DAYS, registerPeopleRoutes, } from './people.js';
|
|
20
22
|
export { PROTECTED_APPS, registerAppRoutes } from './routes.js';
|
|
@@ -76,6 +78,11 @@ export function settingsModule(options = {}) {
|
|
|
76
78
|
actions: {
|
|
77
79
|
'settings.action_overview': { type: 'client', page: 'settings.Overview', label: 'Settings' },
|
|
78
80
|
'settings.action_apps': { type: 'client', page: 'settings.Apps', label: 'Apps' },
|
|
81
|
+
'settings.action_app_settings': {
|
|
82
|
+
type: 'client',
|
|
83
|
+
page: 'settings.AppSettings',
|
|
84
|
+
label: 'App settings',
|
|
85
|
+
},
|
|
79
86
|
'settings.action_company': { type: 'client', page: 'settings.Company', label: 'Company' },
|
|
80
87
|
'settings.action_companies': {
|
|
81
88
|
type: 'client',
|
|
@@ -164,11 +171,19 @@ export function settingsModule(options = {}) {
|
|
|
164
171
|
action: 'settings.action_apps',
|
|
165
172
|
sequence: 1,
|
|
166
173
|
},
|
|
174
|
+
{
|
|
175
|
+
id: 'settings.app_settings',
|
|
176
|
+
parent: 'settings.group_apps',
|
|
177
|
+
label: 'App settings',
|
|
178
|
+
action: 'settings.action_app_settings',
|
|
179
|
+
sequence: 2,
|
|
180
|
+
},
|
|
167
181
|
],
|
|
168
182
|
routes: (context) => {
|
|
169
183
|
registerAppRoutes(context);
|
|
170
184
|
registerCompanyRoutes(context);
|
|
171
185
|
registerSecurityRoutes(context);
|
|
186
|
+
registerAppSettingsRoutes(context);
|
|
172
187
|
registerPeopleRoutes(context, {
|
|
173
188
|
baseUrl: options.baseUrl ?? process.env['KOSH_BASE_URL'] ?? 'http://localhost:5173',
|
|
174
189
|
mailer: 'mailer' in options ? options.mailer : mailerFromEnv(),
|
package/dist/settings/people.js
CHANGED
|
@@ -280,16 +280,29 @@ export function registerPeopleRoutes(context, options) {
|
|
|
280
280
|
};
|
|
281
281
|
});
|
|
282
282
|
});
|
|
283
|
+
// An invitation (the person chooses their password), or — with `password` — a person added directly: the
|
|
284
|
+
// manager sets the password and passes it on; the login may then be a plain name (`rahim`) and the email optional.
|
|
283
285
|
app.post('/api/v1/settings/people', async (request, reply) => {
|
|
284
286
|
const body = (request.body ?? {});
|
|
285
287
|
const name = String(body['name'] ?? '').trim();
|
|
286
|
-
const
|
|
288
|
+
const address = String(body['email'] ?? '')
|
|
287
289
|
.trim()
|
|
288
290
|
.toLowerCase();
|
|
291
|
+
const password = typeof body['password'] === 'string' ? body['password'] : undefined;
|
|
292
|
+
const direct = password !== undefined;
|
|
293
|
+
const login = direct
|
|
294
|
+
? String(body['login'] ?? address)
|
|
295
|
+
.trim()
|
|
296
|
+
.toLowerCase()
|
|
297
|
+
: address;
|
|
289
298
|
if (name === '' || name.length > 200)
|
|
290
299
|
throw bad('The name is 1–200 characters.');
|
|
291
|
-
if (!EMAIL.test(login))
|
|
300
|
+
if (!direct && !EMAIL.test(login))
|
|
292
301
|
throw bad('The email is their login and where the invitation goes.');
|
|
302
|
+
if (direct && !/^[a-z0-9._@+-]{3,100}$/.test(login))
|
|
303
|
+
throw bad('The login is 3–100 letters, digits, or . _ @ + -.');
|
|
304
|
+
if (direct && address !== '' && !EMAIL.test(address))
|
|
305
|
+
throw bad('The email is not valid.');
|
|
293
306
|
const made = await guarded(request, async (env, su, manager) => {
|
|
294
307
|
const assignments = await checkAssignments(su, body['assignments'] ?? [], manager);
|
|
295
308
|
const taken = await ref(withArchived(su), 'res.users').search({ where: { login }, limit: 1 });
|
|
@@ -303,16 +316,27 @@ export function registerPeopleRoutes(context, options) {
|
|
|
303
316
|
{
|
|
304
317
|
login,
|
|
305
318
|
name,
|
|
306
|
-
email:
|
|
319
|
+
email: address === '' ? null : address,
|
|
307
320
|
company: home,
|
|
308
321
|
companies: { set: companies.length === 0 ? [env.companyId] : companies },
|
|
309
322
|
},
|
|
310
323
|
]);
|
|
311
324
|
const user = created.ids[0];
|
|
312
|
-
//
|
|
313
|
-
await auth.setPassword(su, user, randomBytes(32).toString('base64url'));
|
|
325
|
+
// Added directly: the manager's password (checked for length); invited: none usable until the link is used.
|
|
326
|
+
await auth.setPassword(su, user, password ?? randomBytes(32).toString('base64url'));
|
|
314
327
|
if (assignments.length > 0)
|
|
315
328
|
await ref(su, 'auth.user_group').create(assignments.map((a) => ({ user, group: a.group, company: a.company })));
|
|
329
|
+
if (direct) {
|
|
330
|
+
await audit(env, {
|
|
331
|
+
kind: 'people.created',
|
|
332
|
+
summary: `${name} (${login}) added with a password and ${assignments.length} role(s).`,
|
|
333
|
+
targetModel: 'res.users',
|
|
334
|
+
targetId: user,
|
|
335
|
+
details: { assignments },
|
|
336
|
+
ip: request.ip,
|
|
337
|
+
});
|
|
338
|
+
return { id: user, login, activationUrl: null, expiresAt: null, emailed: false };
|
|
339
|
+
}
|
|
316
340
|
const link = await invite(su, user, env.uid);
|
|
317
341
|
await audit(env, {
|
|
318
342
|
kind: 'people.invited',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings › App settings (RFC 0020): the apps' own settings, declared in their manifests. The apps you may see on
|
|
3
|
+
* the left; an app's sections as cards — each setting with its help, a mark when it differs from the default with
|
|
4
|
+
* *Reset*, and *Save* per section, sending only what changed. Company settings name the company they apply to. The
|
|
5
|
+
* server checks every value and who may change it, and audits each change. Each setting opens its history (who, when,
|
|
6
|
+
* old → new); administrators copy a company's settings to another, export them as a file and import one, every one
|
|
7
|
+
* of those previewed before anything is written.
|
|
8
|
+
*/
|
|
9
|
+
import { type PageProps } from '../../ui/index.js';
|
|
10
|
+
import { type ReactElement } from 'react';
|
|
11
|
+
export declare function AppSettingsPage({ api }: PageProps): ReactElement;
|