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.
Files changed (381) hide show
  1. package/dist/account_portal/.kosh/bindings.js +1 -1
  2. package/dist/account_portal/.kosh/schema.d.ts +54 -3
  3. package/dist/account_portal/module.d.ts +359 -6
  4. package/dist/accounting/.kosh/bindings.js +1 -1
  5. package/dist/accounting/.kosh/schema.d.ts +54 -3
  6. package/dist/accounting/index.js +214 -2
  7. package/dist/accounting/module.d.ts +217 -5
  8. package/dist/accounting/module.js +23 -2
  9. package/dist/calendar/.kosh/bindings.d.ts +22 -0
  10. package/dist/calendar/.kosh/bindings.js +15 -0
  11. package/dist/calendar/.kosh/schema.d.ts +795 -0
  12. package/dist/calendar/demo.d.ts +6 -0
  13. package/dist/calendar/demo.js +24 -0
  14. package/dist/calendar/index.d.ts +3 -0
  15. package/dist/calendar/index.js +291 -0
  16. package/dist/calendar/module.d.ts +1705 -0
  17. package/dist/calendar/module.js +180 -0
  18. package/dist/cli/init.js +48 -0
  19. package/dist/cli/templates.d.ts +1 -1
  20. package/dist/cli/templates.js +137 -0
  21. package/dist/contacts/.kosh/bindings.js +1 -1
  22. package/dist/contacts/.kosh/schema.d.ts +78 -3
  23. package/dist/contacts/demo.js +30 -1
  24. package/dist/contacts/index.d.ts +1 -1
  25. package/dist/contacts/index.js +155 -9
  26. package/dist/contacts/module.d.ts +338 -2
  27. package/dist/contacts/module.js +82 -5
  28. package/dist/crm/.kosh/bindings.d.ts +22 -0
  29. package/dist/crm/.kosh/bindings.js +15 -0
  30. package/dist/crm/.kosh/schema.d.ts +799 -0
  31. package/dist/crm/demo.d.ts +6 -0
  32. package/dist/crm/demo.js +45 -0
  33. package/dist/crm/index.d.ts +3 -0
  34. package/dist/crm/index.js +361 -0
  35. package/dist/crm/module.d.ts +1843 -0
  36. package/dist/crm/module.js +207 -0
  37. package/dist/event/.kosh/bindings.d.ts +22 -0
  38. package/dist/event/.kosh/bindings.js +15 -0
  39. package/dist/event/.kosh/schema.d.ts +843 -0
  40. package/dist/event/demo.d.ts +6 -0
  41. package/dist/event/demo.js +65 -0
  42. package/dist/event/index.d.ts +14 -0
  43. package/dist/event/index.js +333 -0
  44. package/dist/event/mails.d.ts +15 -0
  45. package/dist/event/mails.js +112 -0
  46. package/dist/event/module.d.ts +2800 -0
  47. package/dist/event/module.js +323 -0
  48. package/dist/event/web/CheckInBox.d.ts +9 -0
  49. package/dist/event/web/CheckInBox.js +87 -0
  50. package/dist/event/web/index.d.ts +11 -0
  51. package/dist/event/web/index.js +16 -0
  52. package/dist/event/web/messages.d.ts +2 -0
  53. package/dist/event/web/messages.js +23 -0
  54. package/dist/event_account/.kosh/bindings.d.ts +22 -0
  55. package/dist/event_account/.kosh/bindings.js +15 -0
  56. package/dist/event_account/.kosh/schema.d.ts +1639 -0
  57. package/dist/event_account/index.d.ts +3 -0
  58. package/dist/event_account/index.js +28 -0
  59. package/dist/event_account/module.d.ts +6968 -0
  60. package/dist/event_account/module.js +90 -0
  61. package/dist/hr/.kosh/bindings.d.ts +22 -0
  62. package/dist/hr/.kosh/bindings.js +15 -0
  63. package/dist/hr/.kosh/schema.d.ts +878 -0
  64. package/dist/hr/demo.d.ts +6 -0
  65. package/dist/hr/demo.js +60 -0
  66. package/dist/hr/index.d.ts +3 -0
  67. package/dist/hr/index.js +483 -0
  68. package/dist/hr/module.d.ts +2611 -0
  69. package/dist/hr/module.js +350 -0
  70. package/dist/l10n_bd/.kosh/bindings.js +1 -1
  71. package/dist/l10n_bd/.kosh/schema.d.ts +54 -3
  72. package/dist/l10n_bd/module.d.ts +359 -6
  73. package/dist/l10n_in/.kosh/bindings.js +1 -1
  74. package/dist/l10n_in/.kosh/schema.d.ts +54 -3
  75. package/dist/l10n_in/module.d.ts +380 -7
  76. package/dist/maintenance/.kosh/bindings.d.ts +22 -0
  77. package/dist/maintenance/.kosh/bindings.js +15 -0
  78. package/dist/maintenance/.kosh/schema.d.ts +911 -0
  79. package/dist/maintenance/demo.d.ts +6 -0
  80. package/dist/maintenance/demo.js +60 -0
  81. package/dist/maintenance/index.d.ts +3 -0
  82. package/dist/maintenance/index.js +422 -0
  83. package/dist/maintenance/module.d.ts +3545 -0
  84. package/dist/maintenance/module.js +446 -0
  85. package/dist/maintenance_stock/.kosh/bindings.d.ts +22 -0
  86. package/dist/maintenance_stock/.kosh/bindings.js +15 -0
  87. package/dist/maintenance_stock/.kosh/schema.d.ts +1371 -0
  88. package/dist/maintenance_stock/index.d.ts +9 -0
  89. package/dist/maintenance_stock/index.js +52 -0
  90. package/dist/maintenance_stock/module.d.ts +6953 -0
  91. package/dist/maintenance_stock/module.js +192 -0
  92. package/dist/modules-sdk/index.d.ts +1 -1
  93. package/dist/modules-sdk/manifest.d.ts +70 -0
  94. package/dist/modules-sdk/manifest.js +35 -1
  95. package/dist/mrp/.kosh/bindings.d.ts +22 -0
  96. package/dist/mrp/.kosh/bindings.js +15 -0
  97. package/dist/mrp/.kosh/schema.d.ts +1378 -0
  98. package/dist/mrp/demo.d.ts +7 -0
  99. package/dist/mrp/demo.js +42 -0
  100. package/dist/mrp/index.d.ts +3 -0
  101. package/dist/mrp/index.js +392 -0
  102. package/dist/mrp/module.d.ts +8608 -0
  103. package/dist/mrp/module.js +695 -0
  104. package/dist/mrp_account/.kosh/bindings.d.ts +22 -0
  105. package/dist/mrp_account/.kosh/bindings.js +15 -0
  106. package/dist/mrp_account/.kosh/schema.d.ts +2106 -0
  107. package/dist/mrp_account/index.d.ts +3 -0
  108. package/dist/mrp_account/index.js +22 -0
  109. package/dist/mrp_account/module.d.ts +20840 -0
  110. package/dist/mrp_account/module.js +123 -0
  111. package/dist/payment/.kosh/bindings.js +1 -1
  112. package/dist/payment/.kosh/schema.d.ts +54 -3
  113. package/dist/payment/module.d.ts +359 -6
  114. package/dist/portal/.kosh/bindings.js +1 -1
  115. package/dist/portal/.kosh/schema.d.ts +78 -3
  116. package/dist/portal/module.d.ts +166 -2
  117. package/dist/pos/.kosh/bindings.d.ts +22 -0
  118. package/dist/pos/.kosh/bindings.js +15 -0
  119. package/dist/pos/.kosh/schema.d.ts +1690 -0
  120. package/dist/pos/accounting.d.ts +10 -0
  121. package/dist/pos/accounting.js +149 -0
  122. package/dist/pos/demo.d.ts +6 -0
  123. package/dist/pos/demo.js +90 -0
  124. package/dist/pos/index.d.ts +9 -0
  125. package/dist/pos/index.js +349 -0
  126. package/dist/pos/module.d.ts +9636 -0
  127. package/dist/pos/module.js +423 -0
  128. package/dist/pos/routes.d.ts +39 -0
  129. package/dist/pos/routes.js +368 -0
  130. package/dist/pos/web/Register.d.ts +9 -0
  131. package/dist/pos/web/Register.js +252 -0
  132. package/dist/pos/web/index.d.ts +11 -0
  133. package/dist/pos/web/index.js +16 -0
  134. package/dist/pos/web/messages.d.ts +2 -0
  135. package/dist/pos/web/messages.js +87 -0
  136. package/dist/pos_stock/.kosh/bindings.d.ts +22 -0
  137. package/dist/pos_stock/.kosh/bindings.js +15 -0
  138. package/dist/pos_stock/.kosh/schema.d.ts +2015 -0
  139. package/dist/pos_stock/index.d.ts +9 -0
  140. package/dist/pos_stock/index.js +35 -0
  141. package/dist/pos_stock/module.d.ts +11935 -0
  142. package/dist/pos_stock/module.js +115 -0
  143. package/dist/product/.kosh/bindings.js +1 -1
  144. package/dist/product/.kosh/schema.d.ts +51 -1
  145. package/dist/product/index.js +81 -4
  146. package/dist/product/module.d.ts +43 -1
  147. package/dist/product/module.js +9 -2
  148. package/dist/project/.kosh/bindings.js +1 -1
  149. package/dist/project/.kosh/schema.d.ts +54 -3
  150. package/dist/project/index.js +147 -7
  151. package/dist/project/module.d.ts +210 -2
  152. package/dist/project/module.js +52 -5
  153. package/dist/project_portal/.kosh/bindings.js +1 -1
  154. package/dist/project_portal/.kosh/schema.d.ts +54 -3
  155. package/dist/project_portal/module.d.ts +353 -4
  156. package/dist/purchase/.kosh/bindings.js +1 -1
  157. package/dist/purchase/.kosh/schema.d.ts +54 -3
  158. package/dist/purchase/index.js +163 -3
  159. package/dist/purchase/module.d.ts +215 -3
  160. package/dist/purchase/module.js +21 -3
  161. package/dist/purchase_account/.kosh/bindings.js +1 -1
  162. package/dist/purchase_account/.kosh/schema.d.ts +55 -3
  163. package/dist/purchase_account/index.js +26 -1
  164. package/dist/purchase_account/module.d.ts +393 -7
  165. package/dist/purchase_portal/.kosh/bindings.js +1 -1
  166. package/dist/purchase_portal/.kosh/schema.d.ts +54 -3
  167. package/dist/purchase_portal/module.d.ts +366 -5
  168. package/dist/purchase_stock/.kosh/bindings.d.ts +22 -0
  169. package/dist/purchase_stock/.kosh/bindings.js +15 -0
  170. package/dist/purchase_stock/.kosh/schema.d.ts +1310 -0
  171. package/dist/purchase_stock/index.d.ts +7 -0
  172. package/dist/purchase_stock/index.js +91 -0
  173. package/dist/purchase_stock/module.d.ts +6763 -0
  174. package/dist/purchase_stock/module.js +144 -0
  175. package/dist/purchase_stock_account/.kosh/bindings.d.ts +22 -0
  176. package/dist/purchase_stock_account/.kosh/bindings.js +15 -0
  177. package/dist/purchase_stock_account/.kosh/schema.d.ts +1959 -0
  178. package/dist/purchase_stock_account/index.d.ts +3 -0
  179. package/dist/purchase_stock_account/index.js +24 -0
  180. package/dist/purchase_stock_account/module.d.ts +35366 -0
  181. package/dist/purchase_stock_account/module.js +162 -0
  182. package/dist/recruitment/.kosh/bindings.d.ts +22 -0
  183. package/dist/recruitment/.kosh/bindings.js +15 -0
  184. package/dist/recruitment/.kosh/schema.d.ts +990 -0
  185. package/dist/recruitment/demo.d.ts +6 -0
  186. package/dist/recruitment/demo.js +45 -0
  187. package/dist/recruitment/index.d.ts +3 -0
  188. package/dist/recruitment/index.js +385 -0
  189. package/dist/recruitment/module.d.ts +3247 -0
  190. package/dist/recruitment/module.js +193 -0
  191. package/dist/recruitment/web/HireButton.d.ts +9 -0
  192. package/dist/recruitment/web/HireButton.js +32 -0
  193. package/dist/recruitment/web/index.d.ts +11 -0
  194. package/dist/recruitment/web/index.js +16 -0
  195. package/dist/recruitment/web/messages.d.ts +2 -0
  196. package/dist/recruitment/web/messages.js +19 -0
  197. package/dist/reporting/documents.d.ts +5 -0
  198. package/dist/reporting/documents.js +28 -1
  199. package/dist/reporting/index.d.ts +1 -1
  200. package/dist/reporting/index.js +4 -2
  201. package/dist/reporting/models.js +9 -0
  202. package/dist/reporting/overviews.d.ts +16 -0
  203. package/dist/reporting/overviews.js +56 -0
  204. package/dist/reporting/routes-documents.js +1 -1
  205. package/dist/reporting/routes.js +138 -80
  206. package/dist/reporting/web/DashboardView.d.ts +11 -0
  207. package/dist/reporting/web/DashboardView.js +2 -2
  208. package/dist/reporting/web/DesignerView.js +5 -0
  209. package/dist/reporting/web/Overview.d.ts +9 -0
  210. package/dist/reporting/web/Overview.js +53 -0
  211. package/dist/reporting/web/index.d.ts +1 -0
  212. package/dist/reporting/web/index.js +4 -1
  213. package/dist/reporting/web/messages.js +9 -0
  214. package/dist/sale_account/.kosh/bindings.js +1 -1
  215. package/dist/sale_account/.kosh/schema.d.ts +59 -3
  216. package/dist/sale_account/index.js +22 -1
  217. package/dist/sale_account/module.d.ts +461 -7
  218. package/dist/sale_portal/.kosh/bindings.js +1 -1
  219. package/dist/sale_portal/.kosh/schema.d.ts +58 -3
  220. package/dist/sale_portal/module.d.ts +434 -5
  221. package/dist/sale_project/.kosh/bindings.js +1 -1
  222. package/dist/sale_project/.kosh/schema.d.ts +60 -3
  223. package/dist/sale_project/module.d.ts +476 -6
  224. package/dist/sale_stock/.kosh/bindings.d.ts +22 -0
  225. package/dist/sale_stock/.kosh/bindings.js +15 -0
  226. package/dist/sale_stock/.kosh/schema.d.ts +1228 -0
  227. package/dist/sale_stock/index.d.ts +7 -0
  228. package/dist/sale_stock/index.js +84 -0
  229. package/dist/sale_stock/module.d.ts +6791 -0
  230. package/dist/sale_stock/module.js +146 -0
  231. package/dist/sales/.kosh/bindings.js +1 -1
  232. package/dist/sales/.kosh/schema.d.ts +58 -3
  233. package/dist/sales/index.js +191 -4
  234. package/dist/sales/module.d.ts +355 -3
  235. package/dist/sales/module.js +78 -3
  236. package/dist/server/api/metadata.d.ts +5 -0
  237. package/dist/server/api/metadata.js +5 -0
  238. package/dist/server/api/rest.js +64 -1
  239. package/dist/server/api/views.js +1 -0
  240. package/dist/server/app-settings.d.ts +7 -0
  241. package/dist/server/app-settings.js +10 -0
  242. package/dist/server/auth/base.js +9 -0
  243. package/dist/server/index.d.ts +1 -0
  244. package/dist/server/index.js +1 -0
  245. package/dist/settings/app-settings.d.ts +2 -0
  246. package/dist/settings/app-settings.js +518 -0
  247. package/dist/settings/index.d.ts +1 -0
  248. package/dist/settings/index.js +15 -0
  249. package/dist/settings/people.js +29 -5
  250. package/dist/settings/web/AppSettingsPage.d.ts +11 -0
  251. package/dist/settings/web/AppSettingsPage.js +210 -0
  252. package/dist/settings/web/OverviewPage.js +16 -3
  253. package/dist/settings/web/PeoplePage.js +20 -6
  254. package/dist/settings/web/index.d.ts +1 -0
  255. package/dist/settings/web/index.js +3 -0
  256. package/dist/settings/web/messages.js +118 -2
  257. package/dist/stock/.kosh/bindings.d.ts +22 -0
  258. package/dist/stock/.kosh/bindings.js +15 -0
  259. package/dist/stock/.kosh/schema.d.ts +1147 -0
  260. package/dist/stock/demo.d.ts +7 -0
  261. package/dist/stock/demo.js +55 -0
  262. package/dist/stock/index.d.ts +3 -0
  263. package/dist/stock/index.js +674 -0
  264. package/dist/stock/module.d.ts +6330 -0
  265. package/dist/stock/module.js +1325 -0
  266. package/dist/stock/web/ScanBox.d.ts +8 -0
  267. package/dist/stock/web/ScanBox.js +90 -0
  268. package/dist/stock/web/index.d.ts +11 -0
  269. package/dist/stock/web/index.js +16 -0
  270. package/dist/stock/web/messages.d.ts +2 -0
  271. package/dist/stock/web/messages.js +25 -0
  272. package/dist/stock_account/.kosh/bindings.d.ts +22 -0
  273. package/dist/stock_account/.kosh/bindings.js +15 -0
  274. package/dist/stock_account/.kosh/schema.d.ts +1875 -0
  275. package/dist/stock_account/index.d.ts +3 -0
  276. package/dist/stock_account/index.js +130 -0
  277. package/dist/stock_account/module.d.ts +10655 -0
  278. package/dist/stock_account/module.js +339 -0
  279. package/dist/storage/attachments.d.ts +5 -0
  280. package/dist/storage/attachments.js +12 -1
  281. package/dist/storage/models.js +3 -0
  282. package/dist/storage/routes.js +13 -2
  283. package/dist/todo/.kosh/bindings.d.ts +22 -0
  284. package/dist/todo/.kosh/bindings.js +15 -0
  285. package/dist/todo/.kosh/schema.d.ts +732 -0
  286. package/dist/todo/demo.d.ts +6 -0
  287. package/dist/todo/demo.js +35 -0
  288. package/dist/todo/index.d.ts +3 -0
  289. package/dist/todo/index.js +187 -0
  290. package/dist/todo/module.d.ts +1180 -0
  291. package/dist/todo/module.js +130 -0
  292. package/dist/trade_finance/.kosh/bindings.d.ts +22 -0
  293. package/dist/trade_finance/.kosh/bindings.js +15 -0
  294. package/dist/trade_finance/.kosh/schema.d.ts +884 -0
  295. package/dist/trade_finance/demo.d.ts +6 -0
  296. package/dist/trade_finance/demo.js +63 -0
  297. package/dist/trade_finance/index.d.ts +3 -0
  298. package/dist/trade_finance/index.js +413 -0
  299. package/dist/trade_finance/module.d.ts +3779 -0
  300. package/dist/trade_finance/module.js +505 -0
  301. package/dist/trade_finance_account/.kosh/bindings.d.ts +22 -0
  302. package/dist/trade_finance_account/.kosh/bindings.js +15 -0
  303. package/dist/trade_finance_account/.kosh/schema.d.ts +1711 -0
  304. package/dist/trade_finance_account/index.d.ts +3 -0
  305. package/dist/trade_finance_account/index.js +92 -0
  306. package/dist/trade_finance_account/module.d.ts +7928 -0
  307. package/dist/trade_finance_account/module.js +244 -0
  308. package/dist/trade_finance_purchase/.kosh/bindings.d.ts +22 -0
  309. package/dist/trade_finance_purchase/.kosh/bindings.js +15 -0
  310. package/dist/trade_finance_purchase/.kosh/schema.d.ts +1153 -0
  311. package/dist/trade_finance_purchase/index.d.ts +8 -0
  312. package/dist/trade_finance_purchase/index.js +50 -0
  313. package/dist/trade_finance_purchase/module.d.ts +4252 -0
  314. package/dist/trade_finance_purchase/module.js +44 -0
  315. package/dist/trade_finance_sale/.kosh/bindings.d.ts +22 -0
  316. package/dist/trade_finance_sale/.kosh/bindings.js +15 -0
  317. package/dist/trade_finance_sale/.kosh/schema.d.ts +1071 -0
  318. package/dist/trade_finance_sale/index.d.ts +8 -0
  319. package/dist/trade_finance_sale/index.js +50 -0
  320. package/dist/trade_finance_sale/module.d.ts +4280 -0
  321. package/dist/trade_finance_sale/module.js +44 -0
  322. package/dist/ui/api.d.ts +2 -1
  323. package/dist/ui/api.js +2 -1
  324. package/dist/ui/app.js +2 -1
  325. package/dist/ui/components/AppIcon.js +23 -1
  326. package/dist/ui/components/primitives.d.ts +2 -0
  327. package/dist/ui/components/primitives.js +7 -1
  328. package/dist/ui/i18n/messages.d.ts +23 -0
  329. package/dist/ui/i18n/messages.js +38 -0
  330. package/dist/ui/index.d.ts +1 -0
  331. package/dist/ui/index.js +1 -0
  332. package/dist/ui/offline/queue.d.ts +5 -1
  333. package/dist/ui/offline/queue.js +2 -0
  334. package/dist/ui/registry.d.ts +5 -0
  335. package/dist/ui/shell/Home.js +55 -4
  336. package/dist/ui/views/FormView.js +98 -17
  337. package/dist/ui/views/KanbanView.d.ts +2 -0
  338. package/dist/ui/views/KanbanView.js +32 -3
  339. package/dist/ui/views/ListView.js +108 -11
  340. package/dist/ui/views/StatsRow.d.ts +14 -0
  341. package/dist/ui/views/StatsRow.js +88 -0
  342. package/dist/ui/views/widgets.d.ts +2 -1
  343. package/dist/ui/views/widgets.js +194 -1
  344. package/dist/views/builders.d.ts +19 -2
  345. package/dist/views/builders.js +21 -1
  346. package/dist/views/condition.d.ts +6 -0
  347. package/dist/views/condition.js +20 -0
  348. package/dist/views/documents.d.ts +8 -0
  349. package/dist/views/extend.d.ts +2 -2
  350. package/dist/views/extend.js +11 -1
  351. package/dist/views/index.d.ts +2 -2
  352. package/dist/views/index.js +2 -2
  353. package/dist/views/types.d.ts +52 -5
  354. package/dist/views/validate.js +39 -1
  355. package/dist/website/.kosh/bindings.d.ts +22 -0
  356. package/dist/website/.kosh/bindings.js +15 -0
  357. package/dist/website/.kosh/schema.d.ts +881 -0
  358. package/dist/website/demo.d.ts +6 -0
  359. package/dist/website/demo.js +115 -0
  360. package/dist/website/index.d.ts +9 -0
  361. package/dist/website/index.js +250 -0
  362. package/dist/website/module.d.ts +2987 -0
  363. package/dist/website/module.js +281 -0
  364. package/dist/website/render.d.ts +45 -0
  365. package/dist/website/render.js +102 -0
  366. package/dist/website/routes.d.ts +12 -0
  367. package/dist/website/routes.js +246 -0
  368. package/dist/website/web/PagePreview.d.ts +7 -0
  369. package/dist/website/web/PagePreview.js +33 -0
  370. package/dist/website/web/index.d.ts +11 -0
  371. package/dist/website/web/index.js +16 -0
  372. package/dist/website/web/messages.d.ts +2 -0
  373. package/dist/website/web/messages.js +15 -0
  374. package/dist/website_crm/.kosh/bindings.d.ts +22 -0
  375. package/dist/website_crm/.kosh/bindings.js +15 -0
  376. package/dist/website_crm/.kosh/schema.d.ts +966 -0
  377. package/dist/website_crm/index.d.ts +3 -0
  378. package/dist/website_crm/index.js +39 -0
  379. package/dist/website_crm/module.d.ts +2955 -0
  380. package/dist/website_crm/module.js +44 -0
  381. 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
+ }
@@ -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';
@@ -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(),
@@ -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 login = String(body['email'] ?? '')
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: login,
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
- // No usable password until the invitation is used.
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;