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.
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +156 -1
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +156 -1
- package/dist/assistant/index.js +1 -1
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +156 -1
- package/dist/integrations/index.js +12 -0
- package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
- package/dist/integrations/web/OAuthAppsPage.js +79 -0
- package/dist/integrations/web/index.d.ts +1 -0
- package/dist/integrations/web/index.js +3 -0
- package/dist/integrations/web/messages.js +58 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +156 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +156 -4
- package/dist/l10n_in/module.d.ts +0 -62
- package/dist/l10n_in/module.js +2 -4
- package/dist/messaging/web/index.js +1 -1
- package/dist/notify/web/index.js +1 -1
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +157 -2
- package/dist/payment/bkash.d.ts +28 -0
- package/dist/payment/bkash.js +169 -0
- package/dist/payment/index.d.ts +4 -0
- package/dist/payment/index.js +80 -24
- package/dist/payment/module.d.ts +7 -4
- package/dist/payment/module.js +3 -0
- package/dist/payment/money.d.ts +7 -0
- package/dist/payment/money.js +37 -0
- package/dist/payment/providers.d.ts +13 -2
- package/dist/payment/razorpay.d.ts +12 -0
- package/dist/payment/razorpay.js +149 -0
- package/dist/payment/sslcommerz.js +1 -1
- package/dist/payment/stripe.d.ts +15 -0
- package/dist/payment/stripe.js +182 -0
- package/dist/permissions/admin.d.ts +3 -0
- package/dist/permissions/admin.js +108 -10
- package/dist/permissions/index.d.ts +1 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/roles.d.ts +2 -0
- package/dist/permissions/roles.js +236 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +156 -1
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +156 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +156 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +156 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +156 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +156 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +156 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +156 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +156 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +156 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +156 -1
- package/dist/server/auth/base.js +98 -0
- package/dist/server/auth/oauth.d.ts +21 -0
- package/dist/server/auth/oauth.js +579 -0
- package/dist/server/auth/routes.d.ts +3 -0
- package/dist/server/auth/routes.js +29 -0
- package/dist/server/auth/service.d.ts +56 -0
- package/dist/server/auth/service.js +185 -3
- package/dist/server/identity.d.ts +4 -0
- package/dist/server/images.d.ts +12 -0
- package/dist/server/images.js +139 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +1 -0
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.js +20 -4
- package/dist/settings/company.d.ts +34 -0
- package/dist/settings/company.js +189 -0
- package/dist/settings/index.d.ts +10 -10
- package/dist/settings/index.js +159 -30
- package/dist/settings/people.d.ts +16 -0
- package/dist/settings/people.js +507 -0
- package/dist/settings/security.d.ts +13 -0
- package/dist/settings/security.js +158 -0
- package/dist/settings/web/AuditPage.d.ts +8 -0
- package/dist/settings/web/AuditPage.js +74 -0
- package/dist/settings/web/CompaniesPage.d.ts +8 -0
- package/dist/settings/web/CompaniesPage.js +52 -0
- package/dist/settings/web/CompanyPage.d.ts +9 -0
- package/dist/settings/web/CompanyPage.js +105 -0
- package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
- package/dist/settings/web/ConnectedAppsPage.js +33 -0
- package/dist/settings/web/OverviewPage.d.ts +9 -0
- package/dist/settings/web/OverviewPage.js +77 -0
- package/dist/settings/web/PeoplePage.d.ts +9 -0
- package/dist/settings/web/PeoplePage.js +154 -0
- package/dist/settings/web/ProfilePage.d.ts +14 -0
- package/dist/settings/web/ProfilePage.js +196 -0
- package/dist/settings/web/RolesEditor.d.ts +35 -0
- package/dist/settings/web/RolesEditor.js +49 -0
- package/dist/settings/web/RolesPage.d.ts +9 -0
- package/dist/settings/web/RolesPage.js +296 -0
- package/dist/settings/web/SecurityPage.d.ts +8 -0
- package/dist/settings/web/SecurityPage.js +81 -0
- package/dist/settings/web/index.d.ts +10 -0
- package/dist/settings/web/index.js +31 -1
- package/dist/settings/web/messages.js +594 -0
- package/dist/ui/api.d.ts +8 -0
- package/dist/ui/app.d.ts +5 -0
- package/dist/ui/app.js +143 -33
- package/dist/ui/components/AppIcon.js +11 -1
- package/dist/ui/components/Avatar.d.ts +21 -0
- package/dist/ui/components/Avatar.js +71 -0
- package/dist/ui/components/primitives.d.ts +9 -1
- package/dist/ui/components/primitives.js +12 -4
- package/dist/ui/context.d.ts +10 -1
- package/dist/ui/context.js +60 -2
- package/dist/ui/i18n/messages.d.ts +76 -0
- package/dist/ui/i18n/messages.js +152 -0
- package/dist/ui/index.d.ts +3 -1
- package/dist/ui/index.js +3 -1
- package/dist/ui/lib/accent.d.ts +13 -0
- package/dist/ui/lib/accent.js +36 -0
- package/dist/ui/registry.d.ts +7 -1
- package/dist/ui/registry.js +10 -1
- package/dist/ui/search/ViewTabs.d.ts +14 -0
- package/dist/ui/search/ViewTabs.js +42 -0
- package/dist/ui/shell/Activate.d.ts +6 -0
- package/dist/ui/shell/Activate.js +39 -0
- package/dist/ui/shell/CommandPalette.d.ts +7 -1
- package/dist/ui/shell/CommandPalette.js +16 -2
- package/dist/ui/shell/Consent.d.ts +5 -0
- package/dist/ui/shell/Consent.js +35 -0
- package/dist/ui/shell/Home.js +85 -16
- package/dist/ui/shell/Sidebar.d.ts +27 -0
- package/dist/ui/shell/Sidebar.js +77 -0
- package/dist/ui/shell/TwoFactorSetup.d.ts +5 -0
- package/dist/ui/shell/TwoFactorSetup.js +32 -0
- package/dist/ui/shell/shortcuts.d.ts +3 -0
- package/dist/ui/shell/shortcuts.js +13 -4
- package/dist/ui/styles.css +70 -28
- package/dist/ui/views/DateField.d.ts +10 -0
- package/dist/ui/views/DateField.js +129 -0
- package/dist/ui/views/FormView.js +161 -21
- package/dist/ui/views/ListView.d.ts +2 -0
- package/dist/ui/views/ListView.js +57 -25
- package/dist/ui/views/StatusChip.d.ts +10 -0
- package/dist/ui/views/StatusChip.js +22 -0
- package/dist/ui/views/widgets.d.ts +3 -0
- package/dist/ui/views/widgets.js +26 -8
- package/dist/views/types.d.ts +7 -0
- package/dist/views/validate.js +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The companies (RFC 0018, RFC 0019 §3.4). The current company's name, number grouping and brand colour (the
|
|
3
|
+
* back-office's accent), and its details: every simple field the installed apps add to the company (address,
|
|
4
|
+
* contacts, tax ids, fiscal year…). Administrators and holders of the Settings / Company access level change them;
|
|
5
|
+
* administrators also add companies, whose defaults (journals, sequences, taxes…) the apps then install.
|
|
6
|
+
*
|
|
7
|
+
* - `GET /api/v1/settings/company`
|
|
8
|
+
* - `PATCH /api/v1/settings/company` `{ name?, numberGrouping?, accentColor?, details? }` (`accentColor`:
|
|
9
|
+
* `#rrggbb` or null; `details`: `{ field: value }`)
|
|
10
|
+
* - `GET /api/v1/settings/companies` every company with its currency and people, the currencies
|
|
11
|
+
* - `POST /api/v1/settings/companies` `{ name, currency }`: a new company (administrators)
|
|
12
|
+
*/
|
|
13
|
+
import { audit, HttpProblem, imageVersions, installDefaults, publicApiModels, } from '../server/index.js';
|
|
14
|
+
const model = (env, name) => env.model(name);
|
|
15
|
+
const find = async (env, name, where, keys) => {
|
|
16
|
+
const set = await model(env, name).search({ where, order: ['id asc'] });
|
|
17
|
+
return set.length === 0 ? [] : (await set.load(keys)).read(['id', ...keys]);
|
|
18
|
+
};
|
|
19
|
+
const KEYS = ['name', 'numberGrouping', 'accentColor', 'currency'];
|
|
20
|
+
/** Fields with controls of their own, or not the person's to edit. */
|
|
21
|
+
const OWN = new Set([...KEYS, 'id', 'displayName']);
|
|
22
|
+
const SIMPLE = new Set(['char', 'text', 'integer', 'boolean', 'selection']);
|
|
23
|
+
/** Shown, not changed here: an app's own flow sets them (the chart of accounts of a localization). */
|
|
24
|
+
const SHOWN_ONLY = new Set(['chartTemplate']);
|
|
25
|
+
export function registerCompanyRoutes(context) {
|
|
26
|
+
const { app, registry } = context;
|
|
27
|
+
/** The company's details: its stored simple fields, as the generated API serves them. */
|
|
28
|
+
const detailFields = () => (publicApiModels(registry).get('res.company')?.fields ?? []).filter((f) => f.stored && SIMPLE.has(f.kind) && !OWN.has(f.key));
|
|
29
|
+
const read = async (env, id) => {
|
|
30
|
+
const fields = detailFields();
|
|
31
|
+
const keys = [...KEYS, ...fields.map((f) => f.key)];
|
|
32
|
+
const [row] = (await model(env, 'res.company').browse([id]).load(keys)).read(['id', ...keys]);
|
|
33
|
+
const currency = row?.['currency'];
|
|
34
|
+
const [money] = typeof currency === 'number'
|
|
35
|
+
? await find(env, 'res.currency', { id: currency }, ['name'])
|
|
36
|
+
: [];
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
name: String(row?.['name'] ?? ''),
|
|
40
|
+
numberGrouping: row?.['numberGrouping'] === 'lakh' ? 'lakh' : 'international',
|
|
41
|
+
accentColor: typeof row?.['accentColor'] === 'string' ? row['accentColor'] : null,
|
|
42
|
+
currency: String(money?.['name'] ?? ''),
|
|
43
|
+
details: fields.map((f) => ({
|
|
44
|
+
key: f.key,
|
|
45
|
+
label: f.label,
|
|
46
|
+
kind: f.kind,
|
|
47
|
+
readonly: f.readonly || SHOWN_ONLY.has(f.key),
|
|
48
|
+
options: f.options?.map((o) => ({ value: String(o[0]), label: String(o[1]) })) ?? null,
|
|
49
|
+
value: (row?.[f.key] ?? null),
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const guarded = (request, fn, write = false) => request.env(async (env) => {
|
|
54
|
+
if (!env.administrator && !env.groups.has('settings.company'))
|
|
55
|
+
throw new HttpProblem(403, 'access', 'Company settings', 'Changing the company needs the Administrator role or the Settings / Company access.');
|
|
56
|
+
if (env.companyId === null || env.companyId === undefined)
|
|
57
|
+
throw new HttpProblem(409, 'no-company', 'No company', 'Choose a company first.');
|
|
58
|
+
return env.asSuperuser({ reason: 'company settings' }, async (su) => {
|
|
59
|
+
const out = await fn(su, env.companyId, env);
|
|
60
|
+
if (write)
|
|
61
|
+
await su.flush();
|
|
62
|
+
return out;
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
// The superuser scope is audited: even a read writes its audit record.
|
|
66
|
+
{ write: true });
|
|
67
|
+
app.get('/api/v1/settings/company', async (request) => guarded(request, (su, company) => read(su, company)));
|
|
68
|
+
app.patch('/api/v1/settings/company', async (request) => {
|
|
69
|
+
const body = (request.body ?? {});
|
|
70
|
+
const values = {};
|
|
71
|
+
const invalid = (code, title, detail) => new HttpProblem(422, code, title, detail);
|
|
72
|
+
if (body['name'] !== undefined) {
|
|
73
|
+
const name = String(body['name']).trim();
|
|
74
|
+
if (name === '')
|
|
75
|
+
throw invalid('invalid-name', 'Invalid name', 'The company needs a name.');
|
|
76
|
+
values['name'] = name;
|
|
77
|
+
}
|
|
78
|
+
if (body['numberGrouping'] !== undefined) {
|
|
79
|
+
if (body['numberGrouping'] !== 'international' && body['numberGrouping'] !== 'lakh')
|
|
80
|
+
throw invalid('invalid-grouping', 'Invalid grouping', "Number grouping is 'international' or 'lakh'.");
|
|
81
|
+
values['numberGrouping'] = body['numberGrouping'];
|
|
82
|
+
}
|
|
83
|
+
if (body['accentColor'] !== undefined) {
|
|
84
|
+
const color = body['accentColor'];
|
|
85
|
+
if (color !== null && !(typeof color === 'string' && /^#[0-9a-fA-F]{6}$/.test(color)))
|
|
86
|
+
throw invalid('invalid-color', 'Invalid colour', 'The brand colour is a hex colour such as #4f46e5, or empty for the default.');
|
|
87
|
+
values['accentColor'] = color === null ? null : color.toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
// The details: only the fields offered, each with a value of its kind (Shutro checks sizes and constraints).
|
|
90
|
+
const details = body['details'];
|
|
91
|
+
if (details !== undefined) {
|
|
92
|
+
if (typeof details !== 'object' || details === null || Array.isArray(details))
|
|
93
|
+
throw invalid('invalid-details', 'Invalid details', 'details is { field: value }.');
|
|
94
|
+
const offered = new Map(detailFields().map((f) => [f.key, f]));
|
|
95
|
+
for (const [key, value] of Object.entries(details)) {
|
|
96
|
+
const f = offered.get(key);
|
|
97
|
+
if (f === undefined || f.readonly || SHOWN_ONLY.has(key))
|
|
98
|
+
throw invalid('invalid-details', 'Invalid details', `'${key}' is not changed here.`);
|
|
99
|
+
const fits = value === null ||
|
|
100
|
+
(f.kind === 'boolean'
|
|
101
|
+
? typeof value === 'boolean'
|
|
102
|
+
: f.kind === 'integer'
|
|
103
|
+
? Number.isSafeInteger(value)
|
|
104
|
+
: f.kind === 'selection'
|
|
105
|
+
? (f.options ?? []).some((o) => o[0] === value)
|
|
106
|
+
: typeof value === 'string' && value.length <= 4000);
|
|
107
|
+
if (!fits)
|
|
108
|
+
throw invalid('invalid-details', 'Invalid details', `'${f.label}' has an invalid value.`);
|
|
109
|
+
values[key] = typeof value === 'string' && value.trim() === '' ? null : value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return guarded(request, async (su, company) => {
|
|
113
|
+
if (Object.keys(values).length > 0)
|
|
114
|
+
await model(su, 'res.company').browse([company]).write(values);
|
|
115
|
+
return read(su, company);
|
|
116
|
+
}, true);
|
|
117
|
+
});
|
|
118
|
+
app.get('/api/v1/settings/companies', async (request) => guarded(request, async (su, current, env) => {
|
|
119
|
+
const all = await find(su, 'res.company', {}, ['name', 'currency']);
|
|
120
|
+
const currencies = await find(su, 'res.currency', {}, ['name']);
|
|
121
|
+
const logos = await imageVersions(su, 'company', all.map((c) => c['id']));
|
|
122
|
+
const currencyName = new Map(currencies.map((c) => [c['id'], String(c['name'])]));
|
|
123
|
+
return {
|
|
124
|
+
canCreate: env.administrator,
|
|
125
|
+
companies: (await Promise.all(all.map(async (c) => ({
|
|
126
|
+
id: c['id'],
|
|
127
|
+
name: String(c['name']),
|
|
128
|
+
currency: currencyName.get(c['currency']) ?? '',
|
|
129
|
+
people: (await model(su, 'res.users').search({
|
|
130
|
+
where: { isService: { ne: true }, companies: { any: { id: c['id'] } } },
|
|
131
|
+
})).length,
|
|
132
|
+
current: c['id'] === current,
|
|
133
|
+
logo: logos[String(c['id'])] ?? null,
|
|
134
|
+
// The person can switch to it: it is one of their companies.
|
|
135
|
+
member: (await model(su, 'res.users').search({
|
|
136
|
+
where: { id: env.uid, companies: { any: { id: c['id'] } } },
|
|
137
|
+
})).length > 0,
|
|
138
|
+
})))).sort((a, b) => a.name.localeCompare(b.name)),
|
|
139
|
+
currencies: currencies
|
|
140
|
+
.map((c) => ({ id: c['id'], name: String(c['name']) }))
|
|
141
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
142
|
+
};
|
|
143
|
+
}));
|
|
144
|
+
app.post('/api/v1/settings/companies', async (request, reply) => {
|
|
145
|
+
const body = (request.body ?? {});
|
|
146
|
+
const name = String(body['name'] ?? '').trim();
|
|
147
|
+
const currency = Number(body['currency']);
|
|
148
|
+
if (name === '' || name.length > 200)
|
|
149
|
+
throw new HttpProblem(422, 'invalid-name', 'Invalid name', 'The company needs a name.');
|
|
150
|
+
if (!Number.isSafeInteger(currency) || currency <= 0)
|
|
151
|
+
throw new HttpProblem(422, 'invalid-currency', 'Invalid currency', 'Choose its currency.');
|
|
152
|
+
const made = await request.env(async (env) => {
|
|
153
|
+
if (!env.administrator)
|
|
154
|
+
throw new HttpProblem(403, 'access', 'Administrators only', 'Only an administrator adds companies.');
|
|
155
|
+
return env.asSuperuser({ reason: 'new company' }, async (su) => {
|
|
156
|
+
if ((await find(su, 'res.currency', { id: currency }, ['name'])).length === 0)
|
|
157
|
+
throw new HttpProblem(422, 'invalid-currency', 'Invalid currency', 'Choose its currency.');
|
|
158
|
+
const company = (await model(su, 'res.company').create([{ name, currency }]))
|
|
159
|
+
.ids[0];
|
|
160
|
+
// The administrators and the person who made it can switch to it at once.
|
|
161
|
+
const admins = [...su.access.administratorGroups()];
|
|
162
|
+
const holders = admins.length === 0
|
|
163
|
+
? []
|
|
164
|
+
: await find(su, 'auth.user_group', { group: { in: admins } }, ['user']);
|
|
165
|
+
const users = [...new Set([env.uid, ...holders.map((h) => h['user'])])];
|
|
166
|
+
await model(su, 'res.users')
|
|
167
|
+
.browse(users)
|
|
168
|
+
.write({ companies: { link: [company] } });
|
|
169
|
+
await audit(env, {
|
|
170
|
+
kind: 'settings.company_created',
|
|
171
|
+
summary: `Company ${name} created.`,
|
|
172
|
+
targetModel: 'res.company',
|
|
173
|
+
targetId: company,
|
|
174
|
+
ip: request.ip,
|
|
175
|
+
});
|
|
176
|
+
await su.flush();
|
|
177
|
+
return { company, users };
|
|
178
|
+
});
|
|
179
|
+
}, { write: true });
|
|
180
|
+
for (const user of made.users)
|
|
181
|
+
context.auth?.forget(user);
|
|
182
|
+
// The apps' defaults for it, once it exists (after the commit), as for a company made at install.
|
|
183
|
+
const defaults = await installDefaults(context.shutro, registry.loaded.modules, {
|
|
184
|
+
company: made.company,
|
|
185
|
+
reason: `defaults of company ${name}`,
|
|
186
|
+
});
|
|
187
|
+
return reply.code(201).send({ id: made.company, name, defaults });
|
|
188
|
+
});
|
|
189
|
+
}
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
* @packageDocumentation
|
|
3
|
-
* `shutro-erp/settings` (RFC 0008): the Settings app. Its App manager lists the installed, disabled and available apps and
|
|
4
|
-
* installs, enables or disables them (`/api/v1/settings/apps`, the Settings → Apps page). Every installation has it;
|
|
5
|
-
* it cannot be disabled. Changing apps needs the Administrator role or the `settings.apps` access level.
|
|
6
|
-
*/
|
|
7
|
-
import { defineModule } from '../modules-sdk/index.js';
|
|
1
|
+
import { type KoshModule } from '../modules-sdk/index.js';
|
|
8
2
|
import { orm } from '../server/orm.js';
|
|
3
|
+
import { type PeopleOptions } from './people.js';
|
|
4
|
+
export { type CompanyDetail, type CompanySettings, registerCompanyRoutes } from './company.js';
|
|
5
|
+
export { type Assignment, INVITATION_DAYS, type PeopleOptions, registerPeopleRoutes, } from './people.js';
|
|
9
6
|
export { type AppInfo, type PlanInfo, PROTECTED_APPS, registerAppRoutes } from './routes.js';
|
|
7
|
+
export { registerSecurityRoutes } from './security.js';
|
|
10
8
|
/** Package identity. */
|
|
11
9
|
export declare const PACKAGE: {
|
|
12
10
|
readonly name: string;
|
|
13
11
|
readonly rfc: string;
|
|
14
12
|
};
|
|
15
|
-
/** The Shutro module: the
|
|
13
|
+
/** The Shutro module: the Settings groups and invitations. */
|
|
16
14
|
export declare const settingsShutro: ReturnType<typeof orm.defineModule>;
|
|
17
|
-
/** The Settings app. */
|
|
18
|
-
export declare
|
|
15
|
+
/** The Settings app with explicit options (tests): the back-office's address and the mailer of invitations. */
|
|
16
|
+
export declare function settingsModule(options?: Partial<PeopleOptions>): KoshModule;
|
|
17
|
+
/** The Settings app (the back-office's address and the mailer from the environment). */
|
|
18
|
+
export declare const settings: KoshModule;
|
package/dist/settings/index.js
CHANGED
|
@@ -1,51 +1,180 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @packageDocumentation
|
|
3
|
-
* `shutro-erp/settings` (RFC 0008): the Settings app. Its App manager lists the installed, disabled and
|
|
4
|
-
* installs, enables or disables them (`/api/v1/settings/apps
|
|
5
|
-
*
|
|
3
|
+
* `shutro-erp/settings` (RFC 0008, RFC 0019): the Settings app. Its App manager lists the installed, disabled and
|
|
4
|
+
* available apps and installs, enables or disables them (`/api/v1/settings/apps`); it cannot be disabled. Changing
|
|
5
|
+
* apps needs the Administrator role or the `settings.apps` access level. Its Company page sets the current company's
|
|
6
|
+
* name, number grouping, brand colour and details, and Companies lists them, switches and adds one (`settings.company`, RFC 0018,
|
|
7
|
+
* RFC 0019 §3.4). People (RFC 0019 §3.2) invites, deactivates
|
|
8
|
+
* and gives roles to the installation's users (`settings.people`).
|
|
6
9
|
*/
|
|
10
|
+
import { mailerFromEnv } from '../mail/index.js';
|
|
7
11
|
import { defineModule } from '../modules-sdk/index.js';
|
|
8
12
|
import { base, baseShutro } from '../server/index.js';
|
|
9
13
|
import { orm } from '../server/orm.js';
|
|
14
|
+
import { registerCompanyRoutes } from './company.js';
|
|
15
|
+
import { registerPeopleRoutes } from './people.js';
|
|
10
16
|
import { registerAppRoutes } from './routes.js';
|
|
17
|
+
import { registerSecurityRoutes } from './security.js';
|
|
18
|
+
export { registerCompanyRoutes } from './company.js';
|
|
19
|
+
export { INVITATION_DAYS, registerPeopleRoutes, } from './people.js';
|
|
11
20
|
export { PROTECTED_APPS, registerAppRoutes } from './routes.js';
|
|
21
|
+
export { registerSecurityRoutes } from './security.js';
|
|
12
22
|
/** Package identity. */
|
|
13
23
|
export const PACKAGE = Object.freeze({
|
|
14
24
|
name: 'shutro-erp/settings',
|
|
15
25
|
rfc: 'RFC 0008',
|
|
16
26
|
});
|
|
17
|
-
/**
|
|
27
|
+
/** A one-time link that lets an invited person choose their password. Private: read through the People routes. */
|
|
28
|
+
const Invitation = orm.defineModel('settings.invitation', {
|
|
29
|
+
label: 'Invitation',
|
|
30
|
+
order: ['id desc'],
|
|
31
|
+
access: {},
|
|
32
|
+
fields: {
|
|
33
|
+
user: orm.f.many2one('res.users', { required: true, ondelete: 'cascade' }),
|
|
34
|
+
tokenHash: orm.f.char({ required: true, index: 'unique' }),
|
|
35
|
+
expiresAt: orm.f.datetime({ required: true }),
|
|
36
|
+
usedAt: orm.f.datetime(),
|
|
37
|
+
invitedBy: orm.f.many2one('res.users', { ondelete: 'set null' }),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
/** The Shutro module: the Settings groups and invitations. */
|
|
18
41
|
export const settingsShutro = orm.defineModule({
|
|
19
42
|
name: 'settings',
|
|
20
43
|
version: '0.1.0',
|
|
21
44
|
depends: [baseShutro],
|
|
22
45
|
groups: [
|
|
23
46
|
orm.defineGroup({ code: 'settings.apps', label: 'App manager', implies: ['base.user'] }),
|
|
47
|
+
orm.defineGroup({
|
|
48
|
+
code: 'settings.company',
|
|
49
|
+
label: 'Company settings',
|
|
50
|
+
implies: ['base.user'],
|
|
51
|
+
}),
|
|
52
|
+
orm.defineGroup({ code: 'settings.people', label: 'People manager', implies: ['base.user'] }),
|
|
24
53
|
],
|
|
25
|
-
models: [],
|
|
26
|
-
});
|
|
27
|
-
/** The Settings app. */
|
|
28
|
-
export const settings = defineModule({
|
|
29
|
-
shutro: settingsShutro,
|
|
30
|
-
kosh: '^0.1.0',
|
|
31
|
-
depends: [base],
|
|
32
|
-
app: {
|
|
33
|
-
code: 'settings',
|
|
34
|
-
label: 'Settings',
|
|
35
|
-
icon: 'settings',
|
|
36
|
-
sequence: 1000,
|
|
37
|
-
category: 'Administration',
|
|
38
|
-
summary: 'Apps, and the settings of this installation',
|
|
39
|
-
},
|
|
40
|
-
accessLevels: {
|
|
41
|
-
apps: { label: 'App manager', groups: ['settings.apps'] },
|
|
42
|
-
},
|
|
43
|
-
actions: {
|
|
44
|
-
'settings.action_apps': { type: 'client', page: 'settings.Apps', label: 'Apps' },
|
|
45
|
-
},
|
|
46
|
-
menus: [
|
|
47
|
-
{ id: 'settings.root', label: 'Settings', app: 'settings' },
|
|
48
|
-
{ id: 'settings.apps', parent: 'settings.root', label: 'Apps', action: 'settings.action_apps' },
|
|
49
|
-
],
|
|
50
|
-
routes: (context) => registerAppRoutes(context),
|
|
54
|
+
models: [Invitation],
|
|
51
55
|
});
|
|
56
|
+
/** The Settings app with explicit options (tests): the back-office's address and the mailer of invitations. */
|
|
57
|
+
export function settingsModule(options = {}) {
|
|
58
|
+
return defineModule({
|
|
59
|
+
shutro: settingsShutro,
|
|
60
|
+
kosh: '^0.1.0',
|
|
61
|
+
depends: [base],
|
|
62
|
+
app: {
|
|
63
|
+
code: 'settings',
|
|
64
|
+
label: 'Settings',
|
|
65
|
+
icon: 'settings',
|
|
66
|
+
sequence: 1000,
|
|
67
|
+
category: 'Administration',
|
|
68
|
+
summary: 'Apps, people and the settings of this installation',
|
|
69
|
+
},
|
|
70
|
+
accessLevels: {
|
|
71
|
+
apps: { label: 'App manager', groups: ['settings.apps'] },
|
|
72
|
+
company: { label: 'Company settings', groups: ['settings.company'] },
|
|
73
|
+
people: { label: 'People manager', groups: ['settings.people'] },
|
|
74
|
+
},
|
|
75
|
+
privateModels: ['settings.invitation'],
|
|
76
|
+
actions: {
|
|
77
|
+
'settings.action_overview': { type: 'client', page: 'settings.Overview', label: 'Settings' },
|
|
78
|
+
'settings.action_apps': { type: 'client', page: 'settings.Apps', label: 'Apps' },
|
|
79
|
+
'settings.action_company': { type: 'client', page: 'settings.Company', label: 'Company' },
|
|
80
|
+
'settings.action_companies': {
|
|
81
|
+
type: 'client',
|
|
82
|
+
page: 'settings.Companies',
|
|
83
|
+
label: 'Companies',
|
|
84
|
+
},
|
|
85
|
+
'settings.action_security': { type: 'client', page: 'settings.Security', label: 'Security' },
|
|
86
|
+
'settings.action_audit': { type: 'client', page: 'settings.Audit', label: 'Audit log' },
|
|
87
|
+
'settings.action_people': { type: 'client', page: 'settings.People', label: 'People' },
|
|
88
|
+
'settings.action_roles': {
|
|
89
|
+
type: 'client',
|
|
90
|
+
page: 'settings.Roles',
|
|
91
|
+
label: 'Roles & permissions',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
menus: [
|
|
95
|
+
{ id: 'settings.root', label: 'Settings', app: 'settings' },
|
|
96
|
+
// The overview first (the app's home), then the sections in groups (RFC 0019 §3.1). Modules add their
|
|
97
|
+
// settings under `settings.group_apps`.
|
|
98
|
+
{
|
|
99
|
+
id: 'settings.overview',
|
|
100
|
+
parent: 'settings.root',
|
|
101
|
+
label: 'Overview',
|
|
102
|
+
action: 'settings.action_overview',
|
|
103
|
+
sequence: 0,
|
|
104
|
+
},
|
|
105
|
+
{ id: 'settings.group_workspace', parent: 'settings.root', label: 'Workspace', sequence: 10 },
|
|
106
|
+
{
|
|
107
|
+
id: 'settings.company',
|
|
108
|
+
parent: 'settings.group_workspace',
|
|
109
|
+
label: 'Company',
|
|
110
|
+
action: 'settings.action_company',
|
|
111
|
+
sequence: 1,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'settings.companies',
|
|
115
|
+
parent: 'settings.group_workspace',
|
|
116
|
+
label: 'Companies',
|
|
117
|
+
action: 'settings.action_companies',
|
|
118
|
+
sequence: 2,
|
|
119
|
+
},
|
|
120
|
+
{ id: 'settings.group_people', parent: 'settings.root', label: 'People', sequence: 20 },
|
|
121
|
+
{
|
|
122
|
+
id: 'settings.people',
|
|
123
|
+
parent: 'settings.group_people',
|
|
124
|
+
label: 'People',
|
|
125
|
+
action: 'settings.action_people',
|
|
126
|
+
sequence: 1,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'settings.roles',
|
|
130
|
+
parent: 'settings.group_people',
|
|
131
|
+
label: 'Roles & permissions',
|
|
132
|
+
action: 'settings.action_roles',
|
|
133
|
+
sequence: 2,
|
|
134
|
+
},
|
|
135
|
+
// Security: administrators only (the server refuses everyone else).
|
|
136
|
+
{
|
|
137
|
+
id: 'settings.group_security',
|
|
138
|
+
parent: 'settings.root',
|
|
139
|
+
label: 'Security',
|
|
140
|
+
sequence: 30,
|
|
141
|
+
visibleTo: ['base.admin'],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'settings.security',
|
|
145
|
+
parent: 'settings.group_security',
|
|
146
|
+
label: 'Security',
|
|
147
|
+
action: 'settings.action_security',
|
|
148
|
+
sequence: 1,
|
|
149
|
+
visibleTo: ['base.admin'],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'settings.audit',
|
|
153
|
+
parent: 'settings.group_security',
|
|
154
|
+
label: 'Audit log',
|
|
155
|
+
action: 'settings.action_audit',
|
|
156
|
+
sequence: 2,
|
|
157
|
+
visibleTo: ['base.admin'],
|
|
158
|
+
},
|
|
159
|
+
{ id: 'settings.group_apps', parent: 'settings.root', label: 'Apps', sequence: 40 },
|
|
160
|
+
{
|
|
161
|
+
id: 'settings.apps',
|
|
162
|
+
parent: 'settings.group_apps',
|
|
163
|
+
label: 'App manager',
|
|
164
|
+
action: 'settings.action_apps',
|
|
165
|
+
sequence: 1,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
routes: (context) => {
|
|
169
|
+
registerAppRoutes(context);
|
|
170
|
+
registerCompanyRoutes(context);
|
|
171
|
+
registerSecurityRoutes(context);
|
|
172
|
+
registerPeopleRoutes(context, {
|
|
173
|
+
baseUrl: options.baseUrl ?? process.env['KOSH_BASE_URL'] ?? 'http://localhost:5173',
|
|
174
|
+
mailer: 'mailer' in options ? options.mailer : mailerFromEnv(),
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** The Settings app (the back-office's address and the mailer from the environment). */
|
|
180
|
+
export const settings = settingsModule();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Mailer } from '../mail/index.js';
|
|
2
|
+
import { type ModuleContext } from '../server/index.js';
|
|
3
|
+
/** How long an invitation works. */
|
|
4
|
+
export declare const INVITATION_DAYS = 7;
|
|
5
|
+
export interface PeopleOptions {
|
|
6
|
+
/** The back-office's address (`SHUTRO_ERP_BASE_URL`): activation links point there. */
|
|
7
|
+
readonly baseUrl: string;
|
|
8
|
+
/** Sends the invitations; without one, the inviter passes the link on. */
|
|
9
|
+
readonly mailer?: Mailer | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface Assignment {
|
|
12
|
+
readonly group: string;
|
|
13
|
+
/** null: every company. */
|
|
14
|
+
readonly company: number | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function registerPeopleRoutes(context: ModuleContext, options: PeopleOptions): void;
|