shutro-erp 0.3.2 → 0.3.4

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 (156) hide show
  1. package/dist/account_portal/.kosh/bindings.js +1 -1
  2. package/dist/account_portal/.kosh/schema.d.ts +156 -1
  3. package/dist/accounting/.kosh/bindings.js +1 -1
  4. package/dist/accounting/.kosh/schema.d.ts +156 -1
  5. package/dist/assistant/index.js +1 -1
  6. package/dist/contacts/.kosh/bindings.js +1 -1
  7. package/dist/contacts/.kosh/schema.d.ts +156 -1
  8. package/dist/integrations/index.js +12 -0
  9. package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
  10. package/dist/integrations/web/OAuthAppsPage.js +79 -0
  11. package/dist/integrations/web/index.d.ts +1 -0
  12. package/dist/integrations/web/index.js +3 -0
  13. package/dist/integrations/web/messages.js +58 -0
  14. package/dist/l10n_bd/.kosh/bindings.js +1 -1
  15. package/dist/l10n_bd/.kosh/schema.d.ts +156 -1
  16. package/dist/l10n_in/.kosh/bindings.js +1 -1
  17. package/dist/l10n_in/.kosh/schema.d.ts +156 -4
  18. package/dist/l10n_in/module.d.ts +0 -62
  19. package/dist/l10n_in/module.js +2 -4
  20. package/dist/messaging/web/index.js +1 -1
  21. package/dist/notify/web/index.js +1 -1
  22. package/dist/payment/.kosh/bindings.js +1 -1
  23. package/dist/payment/.kosh/schema.d.ts +157 -2
  24. package/dist/payment/bkash.d.ts +28 -0
  25. package/dist/payment/bkash.js +169 -0
  26. package/dist/payment/index.d.ts +4 -0
  27. package/dist/payment/index.js +80 -24
  28. package/dist/payment/module.d.ts +7 -4
  29. package/dist/payment/module.js +3 -0
  30. package/dist/payment/money.d.ts +7 -0
  31. package/dist/payment/money.js +37 -0
  32. package/dist/payment/providers.d.ts +13 -2
  33. package/dist/payment/razorpay.d.ts +12 -0
  34. package/dist/payment/razorpay.js +149 -0
  35. package/dist/payment/sslcommerz.js +1 -1
  36. package/dist/payment/stripe.d.ts +15 -0
  37. package/dist/payment/stripe.js +182 -0
  38. package/dist/permissions/admin.d.ts +3 -0
  39. package/dist/permissions/admin.js +108 -10
  40. package/dist/permissions/index.d.ts +1 -0
  41. package/dist/permissions/index.js +3 -0
  42. package/dist/permissions/roles.d.ts +2 -0
  43. package/dist/permissions/roles.js +236 -0
  44. package/dist/portal/.kosh/bindings.js +1 -1
  45. package/dist/portal/.kosh/schema.d.ts +156 -1
  46. package/dist/product/.kosh/bindings.js +1 -1
  47. package/dist/product/.kosh/schema.d.ts +156 -1
  48. package/dist/project/.kosh/bindings.js +1 -1
  49. package/dist/project/.kosh/schema.d.ts +156 -1
  50. package/dist/project_portal/.kosh/bindings.js +1 -1
  51. package/dist/project_portal/.kosh/schema.d.ts +156 -1
  52. package/dist/purchase/.kosh/bindings.js +1 -1
  53. package/dist/purchase/.kosh/schema.d.ts +156 -1
  54. package/dist/purchase_account/.kosh/bindings.js +1 -1
  55. package/dist/purchase_account/.kosh/schema.d.ts +156 -1
  56. package/dist/purchase_portal/.kosh/bindings.js +1 -1
  57. package/dist/purchase_portal/.kosh/schema.d.ts +156 -1
  58. package/dist/sale_account/.kosh/bindings.js +1 -1
  59. package/dist/sale_account/.kosh/schema.d.ts +156 -1
  60. package/dist/sale_portal/.kosh/bindings.js +1 -1
  61. package/dist/sale_portal/.kosh/schema.d.ts +156 -1
  62. package/dist/sale_project/.kosh/bindings.js +1 -1
  63. package/dist/sale_project/.kosh/schema.d.ts +156 -1
  64. package/dist/sales/.kosh/bindings.js +1 -1
  65. package/dist/sales/.kosh/schema.d.ts +156 -1
  66. package/dist/server/auth/base.js +98 -0
  67. package/dist/server/auth/oauth.d.ts +21 -0
  68. package/dist/server/auth/oauth.js +579 -0
  69. package/dist/server/auth/routes.d.ts +3 -0
  70. package/dist/server/auth/routes.js +29 -0
  71. package/dist/server/auth/service.d.ts +56 -0
  72. package/dist/server/auth/service.js +185 -3
  73. package/dist/server/identity.d.ts +4 -0
  74. package/dist/server/images.d.ts +12 -0
  75. package/dist/server/images.js +139 -0
  76. package/dist/server/index.d.ts +2 -1
  77. package/dist/server/index.js +1 -0
  78. package/dist/server/server.d.ts +4 -0
  79. package/dist/server/server.js +20 -4
  80. package/dist/settings/company.d.ts +34 -0
  81. package/dist/settings/company.js +189 -0
  82. package/dist/settings/index.d.ts +10 -10
  83. package/dist/settings/index.js +159 -30
  84. package/dist/settings/people.d.ts +16 -0
  85. package/dist/settings/people.js +507 -0
  86. package/dist/settings/security.d.ts +13 -0
  87. package/dist/settings/security.js +158 -0
  88. package/dist/settings/web/AuditPage.d.ts +8 -0
  89. package/dist/settings/web/AuditPage.js +74 -0
  90. package/dist/settings/web/CompaniesPage.d.ts +8 -0
  91. package/dist/settings/web/CompaniesPage.js +52 -0
  92. package/dist/settings/web/CompanyPage.d.ts +9 -0
  93. package/dist/settings/web/CompanyPage.js +105 -0
  94. package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
  95. package/dist/settings/web/ConnectedAppsPage.js +33 -0
  96. package/dist/settings/web/OverviewPage.d.ts +9 -0
  97. package/dist/settings/web/OverviewPage.js +77 -0
  98. package/dist/settings/web/PeoplePage.d.ts +9 -0
  99. package/dist/settings/web/PeoplePage.js +154 -0
  100. package/dist/settings/web/ProfilePage.d.ts +14 -0
  101. package/dist/settings/web/ProfilePage.js +196 -0
  102. package/dist/settings/web/RolesEditor.d.ts +35 -0
  103. package/dist/settings/web/RolesEditor.js +49 -0
  104. package/dist/settings/web/RolesPage.d.ts +9 -0
  105. package/dist/settings/web/RolesPage.js +296 -0
  106. package/dist/settings/web/SecurityPage.d.ts +8 -0
  107. package/dist/settings/web/SecurityPage.js +81 -0
  108. package/dist/settings/web/index.d.ts +10 -0
  109. package/dist/settings/web/index.js +31 -1
  110. package/dist/settings/web/messages.js +594 -0
  111. package/dist/ui/api.d.ts +8 -0
  112. package/dist/ui/app.d.ts +5 -0
  113. package/dist/ui/app.js +143 -33
  114. package/dist/ui/components/AppIcon.js +11 -1
  115. package/dist/ui/components/Avatar.d.ts +21 -0
  116. package/dist/ui/components/Avatar.js +71 -0
  117. package/dist/ui/components/primitives.d.ts +9 -1
  118. package/dist/ui/components/primitives.js +12 -4
  119. package/dist/ui/context.d.ts +10 -1
  120. package/dist/ui/context.js +60 -2
  121. package/dist/ui/i18n/messages.d.ts +76 -0
  122. package/dist/ui/i18n/messages.js +152 -0
  123. package/dist/ui/index.d.ts +3 -1
  124. package/dist/ui/index.js +3 -1
  125. package/dist/ui/lib/accent.d.ts +13 -0
  126. package/dist/ui/lib/accent.js +36 -0
  127. package/dist/ui/registry.d.ts +7 -1
  128. package/dist/ui/registry.js +10 -1
  129. package/dist/ui/search/ViewTabs.d.ts +14 -0
  130. package/dist/ui/search/ViewTabs.js +42 -0
  131. package/dist/ui/shell/Activate.d.ts +6 -0
  132. package/dist/ui/shell/Activate.js +39 -0
  133. package/dist/ui/shell/CommandPalette.d.ts +7 -1
  134. package/dist/ui/shell/CommandPalette.js +16 -2
  135. package/dist/ui/shell/Consent.d.ts +5 -0
  136. package/dist/ui/shell/Consent.js +35 -0
  137. package/dist/ui/shell/Home.js +85 -16
  138. package/dist/ui/shell/Sidebar.d.ts +27 -0
  139. package/dist/ui/shell/Sidebar.js +77 -0
  140. package/dist/ui/shell/TwoFactorSetup.d.ts +5 -0
  141. package/dist/ui/shell/TwoFactorSetup.js +32 -0
  142. package/dist/ui/shell/shortcuts.d.ts +3 -0
  143. package/dist/ui/shell/shortcuts.js +13 -4
  144. package/dist/ui/styles.css +70 -28
  145. package/dist/ui/views/DateField.d.ts +10 -0
  146. package/dist/ui/views/DateField.js +129 -0
  147. package/dist/ui/views/FormView.js +161 -21
  148. package/dist/ui/views/ListView.d.ts +2 -0
  149. package/dist/ui/views/ListView.js +57 -25
  150. package/dist/ui/views/StatusChip.d.ts +10 -0
  151. package/dist/ui/views/StatusChip.js +22 -0
  152. package/dist/ui/views/widgets.d.ts +3 -0
  153. package/dist/ui/views/widgets.js +26 -8
  154. package/dist/views/types.d.ts +7 -0
  155. package/dist/views/validate.js +4 -0
  156. package/package.json +2 -2
@@ -0,0 +1,296 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Settings › Roles & permissions (RFC 0019 §3.3): every role with what it opens and who holds it; a role's page —
4
+ * simple first (per app, one access level), *Advanced* below (rights per model with code rights locked, feature
5
+ * permissions, record rules, field security) — saved only after a preview of who gains and who loses what. New
6
+ * roles from scratch or copied. Administrators only: the server refuses everyone else.
7
+ */
8
+ import { Alert, ApiError, Badge, Button, Checkbox, Dialog, Input, Label, Select, Textarea, } from '../../ui/index.js';
9
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
10
+ import { Lock, Plus, Search, Trash2 } from 'lucide-react';
11
+ import { useEffect, useId, useMemo, useState } from 'react';
12
+ import { useTranslation } from 'react-i18next';
13
+ const PERMS = ['read', 'create', 'write', 'unlink', 'export', 'import'];
14
+ const RULE_PERMS = ['read', 'write', 'create', 'unlink'];
15
+ const A = '/api/v1/admin';
16
+ const message = (error) => (error instanceof ApiError ? error.message : String(error));
17
+ /** Per app module: its levels, in declared order (lowest first). */
18
+ function useLevels(api) {
19
+ const levels = useQuery({
20
+ queryKey: ['admin', 'access-levels'],
21
+ queryFn: () => api.call('GET', `${A}/access-levels`),
22
+ });
23
+ const apps = useQuery({
24
+ queryKey: ['settings', 'people', 'catalogue'],
25
+ queryFn: () => api.call('GET', '/api/v1/settings/people/catalogue'),
26
+ });
27
+ return useMemo(() => {
28
+ const byModule = new Map();
29
+ for (const l of levels.data?.accessLevels ?? []) {
30
+ const a = apps.data?.apps.find((x) => x.module === l.module);
31
+ const entry = byModule.get(l.module) ?? {
32
+ label: a?.label ?? l.module,
33
+ app: a?.app ?? false,
34
+ levels: [],
35
+ };
36
+ entry.levels.push(l);
37
+ byModule.set(l.module, entry);
38
+ }
39
+ /** Whether a level belongs to an app (the list shows those; technical levels are in every role). */
40
+ const isApp = (code) => {
41
+ const l = levels.data?.accessLevels.find((x) => x.code === code);
42
+ return l !== undefined && byModule.get(l.module)?.app === true;
43
+ };
44
+ const label = (code) => {
45
+ const l = levels.data?.accessLevels.find((x) => x.code === code);
46
+ return l === undefined ? code : `${byModule.get(l.module)?.label ?? l.module} · ${l.label}`;
47
+ };
48
+ return { byModule, label, isApp, ready: levels.data !== undefined };
49
+ }, [levels.data, apps.data]);
50
+ }
51
+ function PreviewPanel({ preview }) {
52
+ const { t } = useTranslation('settings');
53
+ const nothing = preview.people.length === 0 && preview.models.length === 0;
54
+ return (_jsxs("section", { "aria-label": t('roles.preview'), className: "grid gap-3 rounded-md border bg-muted/40 p-4 text-sm", children: [_jsx("h3", { className: "font-semibold", children: t('roles.preview') }), nothing ? _jsx("p", { children: t('roles.noEffect') }) : null, preview.people.length === 0 ? null : (_jsx("ul", { className: "grid gap-1.5", children: preview.people.map((p) => (_jsxs("li", { children: [_jsx("span", { className: "font-medium", children: p.name }), p.gains.length === 0 ? null : (_jsxs("span", { className: "text-success", children: [' ', t('roles.gains', { what: p.gains.join(', ') })] })), p.losses.length === 0 ? null : (_jsxs("span", { className: "text-danger", children: [' ', t('roles.loses', { what: p.losses.join(', ') })] }))] }, p.id))) })), preview.models.length === 0 ? null : (_jsx("ul", { className: "grid gap-1 text-muted-foreground", children: preview.models.map((m) => (_jsxs("li", { children: [m.label, ": ", m.added.map((p) => `+${t(`roles.perm.${p}`)}`).join(' '), ' ', m.removed.map((p) => `−${t(`roles.perm.${p}`)}`).join(' ')] }, m.model))) })), _jsx("p", { className: "text-xs text-muted-foreground", children: preview.holders === 0
55
+ ? t('roles.nobodyYet')
56
+ : t('roles.holders', { count: preview.holders }) })] }));
57
+ }
58
+ function RoleDialog({ api, code, onClose, }) {
59
+ const { t } = useTranslation('settings');
60
+ const id = useId();
61
+ const queryClient = useQueryClient();
62
+ const levels = useLevels(api);
63
+ const role = useQuery({
64
+ queryKey: ['admin', 'role', code],
65
+ queryFn: () => api.call('GET', `${A}/roles/${encodeURIComponent(code)}`),
66
+ });
67
+ const [label, setLabel] = useState('');
68
+ const [chosen, setChosen] = useState({});
69
+ const [own, setOwn] = useState({});
70
+ const [granted, setGranted] = useState(new Set());
71
+ const [preview, setPreview] = useState(null);
72
+ const [confirmDelete, setConfirmDelete] = useState(false);
73
+ const r = role.data;
74
+ useEffect(() => {
75
+ if (r === undefined)
76
+ return;
77
+ setLabel(r.label);
78
+ // The highest level per app the role gives.
79
+ const next = {};
80
+ for (const [module, entry] of levels.byModule)
81
+ next[module] =
82
+ [...entry.levels].reverse().find((l) => r.accessLevels.includes(l.code))?.code ?? 'none';
83
+ setChosen(next);
84
+ setOwn(Object.fromEntries(r.models.map((m) => [m.name, [...m.own]])));
85
+ setGranted(new Set(r.permissions.filter((p) => p.own).map((p) => p.code)));
86
+ setPreview(null);
87
+ }, [r, levels.byModule]);
88
+ const proposal = () => {
89
+ if (r === undefined)
90
+ return {};
91
+ const changed = Object.fromEntries(r.models
92
+ .filter((m) => [...(own[m.name] ?? [])].sort().join() !== [...m.own].sort().join())
93
+ .map((m) => [m.name, own[m.name] ?? []]));
94
+ const allOwn = Object.fromEntries(r.models
95
+ .filter((m) => (own[m.name] ?? []).length > 0)
96
+ .map((m) => [m.name, own[m.name] ?? []]));
97
+ const accessLevels = Object.values(chosen).filter((c) => c !== 'none');
98
+ const levelGroups = new Set([...levels.byModule.values()].flatMap((e) => e.levels.flatMap((l) => l.groups)));
99
+ return {
100
+ changed,
101
+ body: {
102
+ ...(r.builtIn
103
+ ? {}
104
+ : { accessLevels, implies: r.implies.filter((g) => !levelGroups.has(g)) }),
105
+ access: allOwn,
106
+ permissions: [...granted],
107
+ },
108
+ };
109
+ };
110
+ const preview$ = useMutation({
111
+ mutationFn: () => api.call('POST', `${A}/roles/${encodeURIComponent(code)}/preview`, proposal().body),
112
+ onSuccess: setPreview,
113
+ });
114
+ const save = useMutation({
115
+ mutationFn: async () => {
116
+ if (r === undefined)
117
+ return;
118
+ const { changed, body } = proposal();
119
+ if (!r.builtIn)
120
+ await api.call('PUT', `${A}/roles/${encodeURIComponent(code)}`, {
121
+ label,
122
+ accessLevels: body.accessLevels,
123
+ implies: body.implies,
124
+ });
125
+ for (const [model, perms] of Object.entries(changed ?? {}))
126
+ await api.call('PUT', `${A}/roles/${encodeURIComponent(code)}/access/${encodeURIComponent(model)}`, {
127
+ perms,
128
+ });
129
+ const before = new Set(r.permissions.filter((p) => p.own).map((p) => p.code));
130
+ for (const p of granted)
131
+ if (!before.has(p))
132
+ await api.call('PUT', `${A}/roles/${encodeURIComponent(code)}/permissions/${encodeURIComponent(p)}`);
133
+ for (const p of before)
134
+ if (!granted.has(p))
135
+ await api.call('DELETE', `${A}/roles/${encodeURIComponent(code)}/permissions/${encodeURIComponent(p)}`);
136
+ },
137
+ onSuccess: () => {
138
+ setPreview(null);
139
+ void queryClient.invalidateQueries({ queryKey: ['admin'] });
140
+ },
141
+ });
142
+ const remove = useMutation({
143
+ mutationFn: () => api.call('DELETE', `${A}/roles/${encodeURIComponent(code)}`),
144
+ onSuccess: () => {
145
+ void queryClient.invalidateQueries({ queryKey: ['admin'] });
146
+ onClose();
147
+ },
148
+ });
149
+ // A new record rule and a field restriction.
150
+ const [ruleModel, setRuleModel] = useState('');
151
+ const [rulePerms, setRulePerms] = useState(new Set(['read']));
152
+ const [ruleDomain, setRuleDomain] = useState('{ "createdBy": { "$env": "uid" } }');
153
+ const addRule = useMutation({
154
+ mutationFn: () => {
155
+ const domain = JSON.parse(ruleDomain);
156
+ const name = `${code.replace('.', '_')}_${ruleModel.replace(/\./g, '_')}_${Date.now().toString(36)}`;
157
+ return api.call('PUT', `${A}/roles/${encodeURIComponent(code)}/rules/${name}`, {
158
+ model: ruleModel,
159
+ perms: [...rulePerms],
160
+ domain,
161
+ });
162
+ },
163
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['admin', 'role', code] }),
164
+ });
165
+ const deleteRule = useMutation({
166
+ mutationFn: (name) => api.call('DELETE', `${A}/rules/${encodeURIComponent(name)}`),
167
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['admin', 'role', code] }),
168
+ });
169
+ const error = preview$.error ?? save.error ?? remove.error ?? addRule.error ?? deleteRule.error;
170
+ const groupedModels = useMemo(() => {
171
+ const out = new Map();
172
+ for (const m of r?.models ?? []) {
173
+ const key = m.app ?? t('roles.technical');
174
+ out.set(key, [...(out.get(key) ?? []), m]);
175
+ }
176
+ return [...out];
177
+ }, [r, t]);
178
+ return (_jsx(Dialog, { open: true, onOpenChange: (open) => !open && onClose(), title: r?.label ?? code, wide: true, children: r === undefined ? null : (_jsxs("div", { className: "grid gap-5", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [_jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 text-xs", children: r.code }), r.builtIn ? (_jsx(Badge, { tone: "neutral", children: t('roles.builtIn') })) : (_jsx(Badge, { tone: "info", children: t('roles.custom') })), r.administrator ? _jsx(Badge, { tone: "warning", children: t('people.administrator') }) : null, _jsx("span", { className: "text-muted-foreground", children: r.holders.length === 0
179
+ ? t('roles.nobody')
180
+ : t('roles.heldBy', { names: r.holders.map((h) => h.name).join(', ') }) })] }), r.builtIn ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('roles.builtInHint') })) : (_jsxs("div", { className: "grid gap-1.5 sm:max-w-sm", children: [_jsx(Label, { htmlFor: `${id}-label`, children: t('roles.name') }), _jsx(Input, { id: `${id}-label`, value: label, onChange: (e) => setLabel(e.target.value) })] })), _jsxs("section", { className: "grid gap-2", children: [_jsx("h3", { className: "text-sm font-semibold", children: t('roles.apps') }), _jsx("ul", { className: "divide-y rounded-md border", children: [...levels.byModule]
181
+ .filter(([, e]) => e.app)
182
+ .map(([module, entry]) => (_jsxs("li", { className: "flex flex-wrap items-center justify-between gap-3 px-3 py-2", children: [_jsx(Label, { htmlFor: `${id}-${module}`, className: "font-medium", children: entry.label }), r.builtIn ? (_jsx("span", { className: "text-sm text-muted-foreground", children: entry.levels.find((l) => l.code === chosen[module])?.label ??
183
+ t('people.noAccess') })) : (_jsx(Select, { id: `${id}-${module}`, label: entry.label, className: "w-48", value: chosen[module] ?? 'none', options: [
184
+ { value: 'none', label: t('people.noAccess') },
185
+ ...entry.levels.map((l) => ({ value: l.code, label: l.label })),
186
+ ], onChange: (v) => {
187
+ setChosen({ ...chosen, [module]: v });
188
+ setPreview(null);
189
+ } }))] }, module))) })] }), _jsxs("details", { className: "rounded-md border", children: [_jsx("summary", { className: "cursor-pointer px-3 py-2 text-sm font-semibold", children: t('roles.advanced') }), _jsxs("div", { className: "grid gap-5 border-t p-3", children: [_jsxs("section", { className: "grid gap-2", children: [_jsx("h4", { className: "text-sm font-semibold", children: t('roles.models') }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [_jsx(Lock, { className: "inline size-3", "aria-hidden": true }), " ", t('roles.lockedHint')] }), _jsx("div", { className: "max-h-96 overflow-auto rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("caption", { className: "sr-only", children: t('roles.models') }), _jsx("thead", { className: "sticky top-0 bg-surface", children: _jsxs("tr", { className: "border-b text-xs text-muted-foreground", children: [_jsx("th", { scope: "col", className: "px-2 py-1.5 text-start font-medium", children: t('roles.model') }), PERMS.map((p) => (_jsx("th", { scope: "col", className: "px-2 py-1.5 font-medium", children: t(`roles.perm.${p}`) }, p)))] }) }), _jsx("tbody", { children: groupedModels.map(([app, models]) => [
190
+ _jsx("tr", { className: "bg-muted/50", children: _jsx("th", { colSpan: PERMS.length + 1, scope: "colgroup", className: "px-2 py-1 text-start text-xs font-medium", children: app }) }, `h-${app}`),
191
+ ...models.map((m) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsx("th", { scope: "row", className: "px-2 py-1 text-start font-normal", children: m.label }), PERMS.map((p) => {
192
+ const locked = m.locked.includes(p);
193
+ const mine = (own[m.name] ?? []).includes(p);
194
+ return (_jsx("td", { className: "px-2 py-1 text-center", children: _jsx(Checkbox, { "aria-label": `${m.label}: ${t(`roles.perm.${p}`)}`, checked: locked || mine, disabled: locked, onCheckedChange: (c) => {
195
+ const set = new Set(own[m.name] ?? []);
196
+ if (c === true)
197
+ set.add(p);
198
+ else
199
+ set.delete(p);
200
+ setOwn({ ...own, [m.name]: [...set] });
201
+ setPreview(null);
202
+ } }) }, p));
203
+ })] }, m.name))),
204
+ ]) })] }) })] }), _jsxs("section", { className: "grid gap-2", children: [_jsx("h4", { className: "text-sm font-semibold", children: t('roles.permissions') }), _jsx("div", { className: "grid gap-1.5 sm:grid-cols-2", children: r.permissions.map((p) => (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: `${id}-p-${p.code}`, checked: p.locked || granted.has(p.code), disabled: p.locked, onCheckedChange: (c) => {
205
+ const next = new Set(granted);
206
+ if (c === true)
207
+ next.add(p.code);
208
+ else
209
+ next.delete(p.code);
210
+ setGranted(next);
211
+ setPreview(null);
212
+ } }), _jsx(Label, { htmlFor: `${id}-p-${p.code}`, children: p.label })] }, p.code))) })] }), _jsxs("section", { className: "grid gap-2", children: [_jsx("h4", { className: "text-sm font-semibold", children: t('roles.rules') }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('roles.rulesHint') }), r.rules.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('roles.noRules') })) : (_jsx("ul", { className: "divide-y rounded-md border text-sm", children: r.rules.map((rule) => (_jsxs("li", { className: "flex flex-wrap items-center gap-2 px-3 py-2", children: [_jsx("span", { className: "font-medium", children: rule.model }), _jsx("span", { className: "text-muted-foreground", children: rule.perms.join(', ') }), _jsx("code", { className: "min-w-0 flex-1 truncate text-xs", children: JSON.stringify(rule.domain) }), _jsx(Button, { size: "icon", variant: "ghost", "aria-label": t('roles.deleteRule', { model: rule.model }), onClick: () => deleteRule.mutate(rule.name), children: _jsx(Trash2, { "aria-hidden": true }) })] }, rule.name))) })), _jsxs("div", { className: "grid gap-2 rounded-md border p-3 sm:grid-cols-2", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-rule-model`, children: t('roles.model') }), _jsx(Select, { id: `${id}-rule-model`, label: t('roles.model'), value: ruleModel === '' ? null : ruleModel, options: r.models.map((m) => ({ value: m.name, label: m.label })), onChange: setRuleModel })] }), _jsxs("fieldset", { className: "grid gap-1.5", children: [_jsx("legend", { className: "text-sm font-medium", children: t('roles.ruleApplies') }), _jsx("div", { className: "flex flex-wrap gap-3", children: RULE_PERMS.map((p) => (_jsxs("div", { className: "flex items-center gap-1.5 text-sm", children: [_jsx(Checkbox, { id: `${id}-rp-${p}`, checked: rulePerms.has(p), onCheckedChange: (c) => {
213
+ const next = new Set(rulePerms);
214
+ if (c === true)
215
+ next.add(p);
216
+ else
217
+ next.delete(p);
218
+ setRulePerms(next);
219
+ } }), _jsx(Label, { htmlFor: `${id}-rp-${p}`, children: t(`roles.perm.${p}`) })] }, p))) })] }), _jsxs("div", { className: "grid gap-1.5 sm:col-span-2", children: [_jsx(Label, { htmlFor: `${id}-rule-domain`, children: t('roles.ruleCondition') }), _jsx(Textarea, { id: `${id}-rule-domain`, className: "font-mono text-xs", value: ruleDomain, onChange: (e) => setRuleDomain(e.target.value) })] }), _jsx("div", { className: "sm:col-span-2", children: _jsxs(Button, { variant: "secondary", busy: addRule.isPending, disabled: ruleModel === '' || rulePerms.size === 0, onClick: () => addRule.mutate(), children: [_jsx(Plus, { "aria-hidden": true }), t('roles.addRule')] }) })] })] }), r.fields.length === 0 ? null : (_jsxs("section", { className: "grid gap-2", children: [_jsx("h4", { className: "text-sm font-semibold", children: t('roles.fields') }), _jsx("ul", { className: "grid gap-1 text-sm", children: r.fields.map((f) => (_jsxs("li", { children: [f.model, ".", f.field, ":", ' ', f.write.includes(code) ? t('roles.mayEdit') : t('roles.mayRead')] }, `${f.model}.${f.field}`))) })] }))] })] }), preview === null ? null : _jsx(PreviewPanel, { preview: preview }), error === null ? null : _jsx(Alert, { tone: "danger", children: message(error) }), save.isSuccess && preview === null ? (_jsx(Alert, { tone: "info", children: t('roles.saved') })) : null, _jsxs("div", { className: "flex flex-wrap justify-between gap-2", children: [r.builtIn ? (_jsx("span", {})) : confirmDelete ? (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [t('roles.deleteConfirm', { count: r.holders.length }), _jsx(Button, { size: "sm", onClick: () => setConfirmDelete(false), children: t('profile.cancel') }), _jsx(Button, { size: "sm", variant: "danger", busy: remove.isPending, onClick: () => remove.mutate(), children: t('roles.delete') })] })) : (_jsxs(Button, { variant: "ghost", onClick: () => setConfirmDelete(true), children: [_jsx(Trash2, { "aria-hidden": true }), t('roles.delete')] })), preview === null ? (_jsx(Button, { variant: "primary", busy: preview$.isPending, onClick: () => preview$.mutate(), children: t('roles.review') })) : (_jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { onClick: () => setPreview(null), children: t('roles.keepEditing') }), _jsx(Button, { variant: "primary", busy: save.isPending, onClick: () => save.mutate(), children: t('roles.confirmSave') })] }))] })] })) }));
220
+ }
221
+ function NewRoleDialog({ api, roles, open, onOpenChange, onCreated, }) {
222
+ const { t } = useTranslation('settings');
223
+ const id = useId();
224
+ const [label, setLabel] = useState('');
225
+ const [from, setFrom] = useState(null);
226
+ const code = `company.${label
227
+ .toLowerCase()
228
+ .normalize('NFKD')
229
+ .replace(/[^a-z0-9]+/g, '_')
230
+ .replace(/^_+|_+$/g, '')}`;
231
+ const create = useMutation({
232
+ mutationFn: async () => {
233
+ const source = from === null
234
+ ? null
235
+ : await api.call('GET', `${A}/roles/${encodeURIComponent(from)}`);
236
+ await api.call('PUT', `${A}/roles/${encodeURIComponent(code)}`, {
237
+ label: label.trim(),
238
+ implies: source === null ? ['base.user'] : [...source.implies],
239
+ });
240
+ // A copy takes the source's own grants too.
241
+ for (const m of source?.models ?? [])
242
+ if (m.own.length > 0)
243
+ await api.call('PUT', `${A}/roles/${encodeURIComponent(code)}/access/${encodeURIComponent(m.name)}`, {
244
+ perms: m.own,
245
+ });
246
+ for (const p of source?.permissions ?? [])
247
+ if (p.own)
248
+ await api.call('PUT', `${A}/roles/${encodeURIComponent(code)}/permissions/${encodeURIComponent(p.code)}`);
249
+ return code;
250
+ },
251
+ onSuccess: (made) => {
252
+ setLabel('');
253
+ setFrom(null);
254
+ onCreated(made);
255
+ },
256
+ });
257
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, title: t('roles.newTitle'), description: t('roles.newHint'), footer: _jsxs(_Fragment, { children: [_jsx(Button, { onClick: () => onOpenChange(false), children: t('profile.cancel') }), _jsx(Button, { variant: "primary", busy: create.isPending, disabled: code === 'company.', onClick: () => create.mutate(), children: t('roles.create') })] }), children: _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-label`, children: t('roles.name') }), _jsx(Input, { id: `${id}-label`, value: label, onChange: (e) => setLabel(e.target.value) }), code === 'company.' ? null : (_jsx("code", { className: "text-xs text-muted-foreground", children: code }))] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-from`, children: t('roles.copyFrom') }), _jsx(Select, { id: `${id}-from`, label: t('roles.copyFrom'), value: from ?? 'none', options: [
258
+ { value: 'none', label: t('roles.fromScratch') },
259
+ ...roles
260
+ .filter((r) => !r.administrator)
261
+ .map((r) => ({ value: r.code, label: r.label })),
262
+ ], onChange: (v) => setFrom(v === 'none' ? null : v) })] }), create.error === null ? null : _jsx(Alert, { tone: "danger", children: message(create.error) })] }) }));
263
+ }
264
+ export function RolesPage({ api }) {
265
+ const { t } = useTranslation('settings');
266
+ const id = useId();
267
+ const levels = useLevels(api);
268
+ const [q, setQ] = useState('');
269
+ const [kind, setKind] = useState('all');
270
+ const [open, setOpen] = useState(null);
271
+ const [creating, setCreating] = useState(false);
272
+ const roles = useQuery({
273
+ queryKey: ['admin', 'roles'],
274
+ queryFn: () => api.call('GET', `${A}/roles`),
275
+ });
276
+ if (roles.error !== null)
277
+ return (_jsx(Alert, { tone: "danger", children: roles.error instanceof ApiError && roles.error.status === 403
278
+ ? t('roles.adminsOnly')
279
+ : message(roles.error) }));
280
+ const query = q.trim().toLowerCase();
281
+ const list = (roles.data?.roles ?? [])
282
+ .filter((r) => !r.code.startsWith('portal.'))
283
+ .filter((r) => (kind === 'custom' ? !r.builtIn : kind === 'builtIn' ? r.builtIn : true))
284
+ .filter((r) => query === '' || r.label.toLowerCase().includes(query) || r.code.includes(query))
285
+ .sort((a, b) => Number(a.builtIn) - Number(b.builtIn) || a.label.localeCompare(b.label));
286
+ return (_jsxs("div", { className: "mx-auto grid max-w-5xl gap-5", children: [_jsxs("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('roles.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('roles.intro') })] }), _jsxs(Button, { variant: "primary", onClick: () => setCreating(true), children: [_jsx(Plus, { "aria-hidden": true }), t('roles.new')] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("div", { className: "relative min-w-60 flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": true }), _jsx("label", { htmlFor: `${id}-q`, className: "sr-only", children: t('roles.search') }), _jsx(Input, { id: `${id}-q`, type: "search", className: "ps-9", placeholder: t('roles.search'), value: q, onChange: (e) => setQ(e.target.value) })] }), _jsxs("fieldset", { className: "flex rounded-md border p-0.5", children: [_jsx("legend", { className: "sr-only", children: t('roles.kind') }), ['all', 'custom', 'builtIn'].map((k) => (_jsx("button", { type: "button", "aria-pressed": kind === k, onClick: () => setKind(k), className: "rounded px-3 py-1 text-sm aria-pressed:bg-accent aria-pressed:font-medium aria-pressed:text-accent-foreground", children: t(`roles.filter.${k}`) }, k)))] })] }), _jsx("ul", { className: "divide-y rounded-lg border bg-surface", "aria-label": t('roles.title'), children: list.map((r) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => setOpen(r.code), className: "flex w-full flex-wrap items-center gap-3 px-4 py-3 text-start hover:bg-muted/50", children: [_jsxs("span", { className: "grid min-w-52 flex-1", children: [_jsx("span", { className: "font-medium", children: r.label }), _jsx("code", { className: "text-xs text-muted-foreground", children: r.code })] }), _jsxs("span", { className: "flex min-w-52 flex-[2] flex-wrap gap-1", children: [r.administrator ? _jsx(Badge, { tone: "warning", children: t('people.administrator') }) : null, levels.ready
287
+ ? r.accessLevels
288
+ .filter(levels.isApp)
289
+ .slice(0, 4)
290
+ .map((l) => (_jsx(Badge, { tone: "neutral", children: levels.label(l) }, l)))
291
+ : null] }), _jsxs("span", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [r.builtIn ? (_jsx(Badge, { tone: "neutral", children: t('roles.builtIn') })) : (_jsx(Badge, { tone: "info", children: t('roles.custom') })), t('roles.holders', { count: r.holders })] })] }) }, r.code))) }), _jsx(NewRoleDialog, { api: api, roles: roles.data?.roles ?? [], open: creating, onOpenChange: setCreating, onCreated: (code) => {
292
+ setCreating(false);
293
+ void roles.refetch();
294
+ setOpen(code);
295
+ } }), open === null ? null : _jsx(RoleDialog, { api: api, code: open, onClose: () => setOpen(null) })] }));
296
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Settings › Security (RFC 0019 §3.5), administrators: the policy — who must sign in with a second factor (the people
3
+ * it would stop until they set it up are named before saving) and how long a sign-in lasts — and every signed-in
4
+ * device across people, each signed out with one click. The server enforces the policy and audits every change.
5
+ */
6
+ import { type PageProps } from '../../ui/index.js';
7
+ import { type ReactElement } from 'react';
8
+ export declare function SecurityPage({ api }: PageProps): ReactElement;
@@ -0,0 +1,81 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Settings › Security (RFC 0019 §3.5), administrators: the policy — who must sign in with a second factor (the people
4
+ * it would stop until they set it up are named before saving) and how long a sign-in lasts — and every signed-in
5
+ * device across people, each signed out with one click. The server enforces the policy and audits every change.
6
+ */
7
+ import { Alert, ApiError, Badge, Button, Input, Label, useKosh } from '../../ui/index.js';
8
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
9
+ import { Laptop, Monitor, Smartphone } from 'lucide-react';
10
+ import { useId, useState } from 'react';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { deviceOf } from './ProfilePage.js';
13
+ const message = (error) => (error instanceof ApiError ? error.message : String(error));
14
+ function Section({ title, hint, children, }) {
15
+ const id = useId();
16
+ return (_jsxs("section", { "aria-labelledby": id, className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsxs("div", { children: [_jsx("h2", { id: id, className: "font-semibold", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: hint })] }), children] }));
17
+ }
18
+ function PolicyForm({ api, policy }) {
19
+ const { t } = useTranslation('settings');
20
+ const id = useId();
21
+ const queryClient = useQueryClient();
22
+ const [twoFactor, setTwoFactor] = useState(policy.twoFactor);
23
+ const [days, setDays] = useState(policy.sessionDays === null ? '' : String(policy.sessionDays));
24
+ const save = useMutation({
25
+ mutationFn: () => api.call('PATCH', '/api/v1/settings/security', {
26
+ twoFactor,
27
+ sessionDays: days === '' ? null : Number(days),
28
+ }),
29
+ // The form already shows what was saved; the server's answer names who is still without it.
30
+ onSuccess: (saved) => queryClient.setQueryData(['settings', 'security'], saved),
31
+ });
32
+ const stopped = twoFactor === 'everyone' ? policy.withoutTwoFactor : [];
33
+ return (_jsxs("form", { className: "grid gap-4", onSubmit: (e) => {
34
+ e.preventDefault();
35
+ save.mutate();
36
+ }, children: [_jsxs("fieldset", { className: "grid gap-2", children: [_jsx("legend", { className: "mb-1 text-sm font-medium", children: t('security.twoFactorFor') }), ['administrators', 'everyone'].map((value) => (_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "radio", name: `${id}-two-factor`, className: "mt-1 accent-[var(--primary)]", checked: twoFactor === value, onChange: () => setTwoFactor(value) }), _jsxs("span", { children: [_jsx("span", { className: "font-medium", children: t(`security.${value}`) }), _jsx("span", { className: "block text-muted-foreground", children: t(`security.${value}Hint`) })] })] }, value)))] }), stopped.length === 0 ? null : (_jsx(Alert, { tone: "warning", children: t('security.wouldStop', {
37
+ count: stopped.length,
38
+ names: stopped
39
+ .slice(0, 5)
40
+ .map((p) => p.name)
41
+ .join(', '),
42
+ }) })), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-days`, children: t('security.sessionDays') }), _jsx(Input, { id: `${id}-days`, type: "number", min: 1, max: 90, className: "w-32", placeholder: t('security.serverDefault'), value: days, onChange: (e) => setDays(e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('security.sessionDaysHint') })] }), save.error === null ? null : _jsx(Alert, { tone: "danger", children: message(save.error) }), save.isSuccess ? _jsx(Alert, { tone: "info", children: t('security.saved') }) : null, _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", variant: "primary", busy: save.isPending, children: t('security.save') }) })] }));
43
+ }
44
+ function Sessions({ api }) {
45
+ const { t } = useTranslation('settings');
46
+ const { format } = useKosh();
47
+ const queryClient = useQueryClient();
48
+ const [filter, setFilter] = useState('');
49
+ const sessions = useQuery({
50
+ queryKey: ['settings', 'sessions'],
51
+ queryFn: () => api.call('GET', '/api/v1/settings/sessions'),
52
+ });
53
+ const revoke = useMutation({
54
+ mutationFn: (sid) => api.call('DELETE', `/api/v1/settings/sessions/${sid}`),
55
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['settings', 'sessions'] }),
56
+ });
57
+ const words = filter.trim().toLowerCase();
58
+ const list = (sessions.data?.sessions ?? []).filter((s) => words === '' || `${s.name} ${s.login} ${s.ip ?? ''}`.toLowerCase().includes(words));
59
+ return (_jsxs("div", { className: "grid gap-3", children: [_jsx(Input, { "aria-label": t('security.searchSessions'), placeholder: t('security.searchSessions'), className: "max-w-xs", value: filter, onChange: (e) => setFilter(e.target.value) }), sessions.error === null ? null : _jsx(Alert, { tone: "danger", children: message(sessions.error) }), revoke.error === null ? null : _jsx(Alert, { tone: "danger", children: message(revoke.error) }), _jsxs("ul", { className: "divide-y rounded-md border", children: [list.map((s) => {
60
+ const device = deviceOf(s.userAgent);
61
+ const Icon = device.phone ? Smartphone : device.os === '' ? Monitor : Laptop;
62
+ const name = [device.browser, device.os].filter((x) => x !== '').join(' · ') ||
63
+ t('profile.unknownDevice');
64
+ return (_jsxs("li", { className: "flex flex-wrap items-center gap-3 px-3 py-2.5", children: [_jsx(Icon, { className: "size-5 text-muted-foreground", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "text-sm font-medium", children: [s.name, " ", _jsxs("span", { className: "font-normal text-muted-foreground", children: ["\u00B7 ", name] }), ' ', s.current ? _jsx(Badge, { tone: "success", children: t('profile.thisDevice') }) : null] }), _jsx("p", { className: "text-xs text-muted-foreground", children: [
65
+ s.ip,
66
+ s.lastSeenAt === null
67
+ ? null
68
+ : t('profile.lastActive', { when: format.datetime(s.lastSeenAt) }),
69
+ ]
70
+ .filter((x) => x !== null)
71
+ .join(' · ') })] }), s.current ? null : (_jsx(Button, { size: "sm", variant: "secondary", busy: revoke.isPending && revoke.variables === s.id, onClick: () => revoke.mutate(s.id), "aria-label": t('security.signOutOf', { name: s.name, device: name }), children: t('profile.signOut') }))] }, s.id));
72
+ }), sessions.data !== undefined && list.length === 0 ? (_jsx("li", { className: "px-3 py-2.5 text-sm text-muted-foreground", children: t('security.noSessions') })) : null] })] }));
73
+ }
74
+ export function SecurityPage({ api }) {
75
+ const { t } = useTranslation('settings');
76
+ const policy = useQuery({
77
+ queryKey: ['settings', 'security'],
78
+ queryFn: () => api.call('GET', '/api/v1/settings/security'),
79
+ });
80
+ return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('security.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('security.intro') })] }), policy.error !== null ? (_jsx(Alert, { tone: "danger", children: message(policy.error) })) : (_jsxs(_Fragment, { children: [_jsx(Section, { title: t('security.policy'), hint: t('security.policyHint'), children: policy.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (_jsx(PolicyForm, { api: api, policy: policy.data })) }), _jsx(Section, { title: t('security.sessions'), hint: t('security.sessionsHint'), children: _jsx(Sessions, { api: api }) })] }))] }));
81
+ }
@@ -8,6 +8,16 @@ import type { i18n } from 'i18next';
8
8
  import { messages } from './messages.js';
9
9
  export { ApiKeysPage } from './ApiKeysPage.js';
10
10
  export { AppsPage } from './AppsPage.js';
11
+ export { AuditPage } from './AuditPage.js';
12
+ export { CompaniesPage } from './CompaniesPage.js';
13
+ export { CompanyPage } from './CompanyPage.js';
14
+ export { ConnectedAppsPage } from './ConnectedAppsPage.js';
15
+ export { OverviewPage } from './OverviewPage.js';
16
+ export { PeoplePage } from './PeoplePage.js';
17
+ export { deviceOf, ProfilePage } from './ProfilePage.js';
18
+ export { RolesEditor } from './RolesEditor.js';
19
+ export { RolesPage } from './RolesPage.js';
20
+ export { SecurityPage } from './SecurityPage.js';
11
21
  /** Registers the Apps page (and its messages in `i18n`, when given). */
12
22
  export declare function registerSettingsWeb(registry: Registry, i18n?: i18n): Registry;
13
23
  export { messages as settingsMessages };
@@ -6,14 +6,44 @@
6
6
  import { addMessages } from '../../ui/index.js';
7
7
  import { ApiKeysPage } from './ApiKeysPage.js';
8
8
  import { AppsPage } from './AppsPage.js';
9
+ import { AuditPage } from './AuditPage.js';
10
+ import { CompaniesPage } from './CompaniesPage.js';
11
+ import { CompanyPage } from './CompanyPage.js';
12
+ import { ConnectedAppsPage } from './ConnectedAppsPage.js';
9
13
  import { messages } from './messages.js';
14
+ import { OverviewPage } from './OverviewPage.js';
15
+ import { PeoplePage } from './PeoplePage.js';
16
+ import { ProfilePage } from './ProfilePage.js';
17
+ import { RolesPage } from './RolesPage.js';
18
+ import { SecurityPage } from './SecurityPage.js';
10
19
  export { ApiKeysPage } from './ApiKeysPage.js';
11
20
  export { AppsPage } from './AppsPage.js';
21
+ export { AuditPage } from './AuditPage.js';
22
+ export { CompaniesPage } from './CompaniesPage.js';
23
+ export { CompanyPage } from './CompanyPage.js';
24
+ export { ConnectedAppsPage } from './ConnectedAppsPage.js';
25
+ export { OverviewPage } from './OverviewPage.js';
26
+ export { PeoplePage } from './PeoplePage.js';
27
+ export { deviceOf, ProfilePage } from './ProfilePage.js';
28
+ export { RolesEditor } from './RolesEditor.js';
29
+ export { RolesPage } from './RolesPage.js';
30
+ export { SecurityPage } from './SecurityPage.js';
12
31
  /** Registers the Apps page (and its messages in `i18n`, when given). */
13
32
  export function registerSettingsWeb(registry, i18n) {
33
+ registry.registerPage('settings.Overview', OverviewPage);
14
34
  registry.registerPage('settings.Apps', AppsPage);
15
- // Every signed-in user's own keys (RFC 0017 §5.1): in the user menu, not in the Settings app.
35
+ registry.registerPage('settings.People', PeoplePage);
36
+ registry.registerPage('settings.Roles', RolesPage);
37
+ registry.registerPage('settings.Company', CompanyPage);
38
+ registry.registerPage('settings.Companies', CompaniesPage);
39
+ registry.registerPage('settings.Security', SecurityPage);
40
+ registry.registerPage('settings.Audit', AuditPage);
41
+ // Every signed-in user's own profile (RFC 0019) and keys (RFC 0017 §5.1): in the user menu, not in Settings.
42
+ registry.registerUserPage('settings.Profile', ProfilePage, { label: 'settings:profile.title' });
16
43
  registry.registerUserPage('settings.ApiKeys', ApiKeysPage, { label: 'settings:apiKeys.title' });
44
+ registry.registerUserPage('settings.ConnectedApps', ConnectedAppsPage, {
45
+ label: 'settings:connected.title',
46
+ });
17
47
  if (i18n !== undefined)
18
48
  addMessages(i18n, 'settings', messages);
19
49
  return registry;