@coworkee/airworks 0.0.0-stage → 0.0.1023
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/README.md +23 -2
- package/dist/apps/admin/identity.d.ts +15 -0
- package/dist/apps/admin/identity.js +256 -0
- package/dist/apps/admin/index.d.ts +9 -0
- package/dist/apps/admin/index.js +48 -0
- package/dist/apps/admin/mail.d.ts +20 -0
- package/dist/apps/admin/mail.js +255 -0
- package/dist/apps/admin/system.d.ts +12 -0
- package/dist/apps/admin/system.js +251 -0
- package/dist/apps/assistant.d.ts +5 -0
- package/dist/apps/assistant.js +124 -0
- package/dist/apps/chat.d.ts +3 -0
- package/dist/apps/chat.js +117 -0
- package/dist/apps/data.d.ts +12 -0
- package/dist/apps/data.js +53 -0
- package/dist/apps/file-preview.d.ts +4 -0
- package/dist/apps/file-preview.js +32 -0
- package/dist/apps/files.d.ts +14 -0
- package/dist/apps/files.js +139 -0
- package/dist/apps/notifications.d.ts +5 -0
- package/dist/apps/notifications.js +72 -0
- package/dist/apps/profile.d.ts +3 -0
- package/dist/apps/profile.js +40 -0
- package/dist/apps/report-designer.d.ts +6 -0
- package/dist/apps/report-designer.js +308 -0
- package/dist/apps/report-model.d.ts +40 -0
- package/dist/apps/report-model.js +108 -0
- package/dist/apps/reports.d.ts +18 -0
- package/dist/apps/reports.js +173 -0
- package/dist/apps/settings.d.ts +26 -0
- package/dist/apps/settings.js +136 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +27 -0
- package/dist/coworkee.d.ts +63 -0
- package/dist/coworkee.js +113 -0
- package/dist/crud.d.ts +94 -0
- package/dist/crud.js +138 -0
- package/dist/dom.d.ts +9 -0
- package/dist/dom.js +37 -0
- package/dist/form.d.ts +38 -0
- package/dist/form.js +133 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +33 -0
- package/dist/lookup.d.ts +19 -0
- package/dist/lookup.js +61 -0
- package/dist/mudex-frame.html +30 -0
- package/dist/mudex.d.ts +26 -0
- package/dist/mudex.js +36 -0
- package/dist/settings-model.d.ts +20 -0
- package/dist/settings-model.js +35 -0
- package/dist/style.css +180 -0
- package/dist/table.d.ts +29 -0
- package/dist/table.js +87 -0
- package/package.json +49 -4
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @coworkee/airworks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The [AirWorks](https://fgilde.github.io/AirWorks/) web desktop for apps built on [Coworkee](https://fgilde.github.io/CoworkeeLib/): sign-in through the app's BFF, theme, logo and texts of the app, realtime, and apps for profile, settings, files (with the MudEx file preview), data tables, chat, reports, reports with their designer, the AI assistant, notifications and the whole administration (users, roles, applications, themes, languages, translations, mail templates, campaigns, mail log, backups, audit log, ...).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @coworkee/airworks @coworkee/client github:fgilde/AirWorks
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { adminApps, defaultApps, useCoworkee } from '@coworkee/airworks';
|
|
11
|
+
import { AirWorks } from '@fgilde/airworks';
|
|
12
|
+
import '@fgilde/airworks/style.css';
|
|
13
|
+
import '@coworkee/airworks/style.css';
|
|
14
|
+
|
|
15
|
+
const coworkee = await useCoworkee(AirWorks, { title: 'Shop' });
|
|
16
|
+
await coworkee.register(...defaultApps, ...adminApps);
|
|
17
|
+
document.body.append(document.createElement('air-desktop'));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Apps are hidden when the user lacks their permission, like the menu of the Blazor app. `crudApp(...)` builds list apps with create, edit, delete and row actions; `coworkee.realtime.subscribe(topic, handler)` follows the realtime hub.
|
|
21
|
+
|
|
22
|
+
`coworkee new --airworks` creates an app with this client in the Aspire app host; it takes the packages from the tarballs inside the template, or with `--airworks-packages npm` the version of the Coworkee release on npm. Documentation: [AirWorks client](https://fgilde.github.io/CoworkeeLib/web/airworks/)
|
|
23
|
+
|
|
24
|
+
License: MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Coworkee } from '../../coworkee.ts';
|
|
2
|
+
import type { LookupSource } from '../../lookup.ts';
|
|
3
|
+
/** Users searched and paged on the server, for pickers. */
|
|
4
|
+
export declare const userLookup: ({ request }: Coworkee) => LookupSource;
|
|
5
|
+
/** Groups searched and paged on the server, for pickers. */
|
|
6
|
+
export declare const groupLookup: ({ request }: Coworkee) => LookupSource;
|
|
7
|
+
/** The permissions of a role, user or group as one checkbox list per permission group. */
|
|
8
|
+
export declare function editGrants(coworkee: Coworkee, providerType: 'Role' | 'User' | 'Group', key: string, title: string): Promise<boolean>;
|
|
9
|
+
export declare const usersApp: (coworkee: Coworkee) => import("../../coworkee.ts").CoworkeeApp;
|
|
10
|
+
export declare const groupsApp: (coworkee: Coworkee) => import("../../coworkee.ts").CoworkeeApp;
|
|
11
|
+
export declare const rolesApp: (coworkee: Coworkee) => import("../../coworkee.ts").CoworkeeApp;
|
|
12
|
+
export declare const clientsApp: (coworkee: Coworkee) => import("../../coworkee.ts").CoworkeeApp;
|
|
13
|
+
export declare const scopesApp: (coworkee: Coworkee) => import("../../coworkee.ts").CoworkeeApp;
|
|
14
|
+
export declare const apiKeysApp: (coworkee: Coworkee) => import("../../coworkee.ts").CoworkeeApp;
|
|
15
|
+
export declare const identityApps: ((coworkee: Coworkee) => import("../../coworkee.ts").CoworkeeApp)[];
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { choices, crudApp } from "../../crud.js";
|
|
2
|
+
import { display, esc } from "../../dom.js";
|
|
3
|
+
import { formDialog, infoDialog } from "../../form.js";
|
|
4
|
+
const identity = '/api/v1/identity';
|
|
5
|
+
const json = (method, body) => ({ method, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
6
|
+
const quiet = (promise, fallback) => promise.catch(() => fallback);
|
|
7
|
+
const check = (value) => value ? '✓' : '';
|
|
8
|
+
const roleChoices = async (coworkee) => choices(await quiet(coworkee.request(`${identity}/roles`), []), (role) => role.id, (role) => role.name);
|
|
9
|
+
const pageOf = (page, text) => new URLSearchParams({ Page: String(page), PageSize: '20', Search: text });
|
|
10
|
+
/** Users searched and paged on the server, for pickers. */
|
|
11
|
+
export const userLookup = ({ request }) => ({
|
|
12
|
+
search: async (text, page) => {
|
|
13
|
+
const result = await request(`${identity}/users?${pageOf(page, text)}`);
|
|
14
|
+
return { items: result.items.map((user) => ({ value: user.id, label: user.email })), total: Number(result.totalCount) };
|
|
15
|
+
},
|
|
16
|
+
resolve: (ids) => request(`${identity}/users/names`, json('POST', { ids })),
|
|
17
|
+
});
|
|
18
|
+
/** Groups searched and paged on the server, for pickers. */
|
|
19
|
+
export const groupLookup = ({ request }) => ({
|
|
20
|
+
search: async (text, page) => {
|
|
21
|
+
const result = await request(`${identity}/groups?${pageOf(page, text)}`);
|
|
22
|
+
return { items: result.items.map((group) => ({ value: group.id, label: group.name })), total: Number(result.totalCount) };
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
/** The permissions of a role, user or group as one checkbox list per permission group. */
|
|
26
|
+
export async function editGrants(coworkee, providerType, key, title) {
|
|
27
|
+
const path = `${identity}/permissions/grants/${providerType}/${encodeURIComponent(key)}`;
|
|
28
|
+
const [groups, granted] = await Promise.all([coworkee.request(`${identity}/permissions/definitions`), coworkee.request(path)]);
|
|
29
|
+
const fields = groups.map((group) => ({ name: group.name, label: group.displayName, type: 'multi', options: group.permissions.map((p) => ({ value: p.name, label: coworkee.t(p.displayName) })) }));
|
|
30
|
+
const values = Object.fromEntries(groups.map((group) => [group.name, group.permissions.map((p) => p.name).filter((name) => granted.includes(name))]));
|
|
31
|
+
await formDialog(coworkee, {
|
|
32
|
+
title: `${coworkee.t('Permissions')} - ${title}`, fields, values,
|
|
33
|
+
onSubmit: (chosen) => coworkee.request(path, json('PUT', { names: Object.values(chosen).flat() })),
|
|
34
|
+
});
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
export const usersApp = crudApp({
|
|
38
|
+
id: 'coworkee.admin.users', group: 'Identity', title: 'Users', icon: 'startmenu-user.png', accent: '#1565c0', width: 920,
|
|
39
|
+
permissions: ['Identity.Users.View'],
|
|
40
|
+
search: true, serverSearch: true,
|
|
41
|
+
load: async ({ request }, { page, pageSize, search }) => {
|
|
42
|
+
const result = await request(`${identity}/users?${new URLSearchParams({ Page: String(page), PageSize: String(pageSize), Search: search })}`);
|
|
43
|
+
return { items: result.items, total: Number(result.totalCount) };
|
|
44
|
+
},
|
|
45
|
+
columns: [
|
|
46
|
+
{ field: 'email', title: 'Email' },
|
|
47
|
+
{ field: 'name', title: 'Name', format: (user) => [user.firstName, user.lastName].filter(Boolean).join(' ') },
|
|
48
|
+
{ field: 'roles', title: 'Roles', format: (user) => user.roles.map((role) => role.name).join(', ') },
|
|
49
|
+
{ field: 'isActive', title: 'Active', format: (user) => check(user.isActive), html: true },
|
|
50
|
+
],
|
|
51
|
+
create: {
|
|
52
|
+
permission: 'Identity.Users.Manage',
|
|
53
|
+
values: { isActive: true },
|
|
54
|
+
fields: async (coworkee) => [
|
|
55
|
+
{ name: 'email', label: 'Email', type: 'email', required: true },
|
|
56
|
+
{ name: 'firstName', label: 'First name' }, { name: 'lastName', label: 'Last name' },
|
|
57
|
+
{ name: 'password', label: 'Initial password', type: 'password' },
|
|
58
|
+
{ name: 'mustChangePassword', label: 'Must change password', type: 'bool' },
|
|
59
|
+
{ name: 'isActive', label: 'Active', type: 'bool' },
|
|
60
|
+
{ name: 'roleIds', label: 'Roles', type: 'multi', options: await roleChoices(coworkee) },
|
|
61
|
+
],
|
|
62
|
+
save: (values, { request }) => request(`${identity}/users`, json('POST', values)),
|
|
63
|
+
},
|
|
64
|
+
edit: {
|
|
65
|
+
permission: 'Identity.Users.Manage',
|
|
66
|
+
fields: async (coworkee) => [
|
|
67
|
+
{ name: 'userName', label: 'User name' }, { name: 'firstName', label: 'First name' }, { name: 'lastName', label: 'Last name' },
|
|
68
|
+
{ name: 'phoneNumber', label: 'Phone' },
|
|
69
|
+
{ name: 'isActive', label: 'Active', type: 'bool' }, { name: 'emailConfirmed', label: 'Email confirmed', type: 'bool' },
|
|
70
|
+
{ name: 'mustChangePassword', label: 'Must change password', type: 'bool' },
|
|
71
|
+
{ name: 'roleIds', label: 'Roles', type: 'multi', options: await roleChoices(coworkee) },
|
|
72
|
+
...(coworkee.can('Identity.Groups.View') ? [{ name: 'groupIds', label: 'Groups', type: 'lookup', lookup: groupLookup(coworkee) }] : []),
|
|
73
|
+
],
|
|
74
|
+
values: async (user, { request }) => {
|
|
75
|
+
const detail = await request(`${identity}/users/${user.id}`);
|
|
76
|
+
return { ...detail, roleIds: detail.roles.map((role) => role.id), groupIds: detail.groups.map((group) => ({ value: group.id, label: group.name })) };
|
|
77
|
+
},
|
|
78
|
+
save: async (user, values, coworkee) => {
|
|
79
|
+
const detail = await coworkee.request(`${identity}/users/${user.id}`);
|
|
80
|
+
await coworkee.request(`${identity}/users/${user.id}`, json('PUT', {
|
|
81
|
+
firstName: values.firstName, lastName: values.lastName, userName: values.userName, phoneNumber: values.phoneNumber, address: detail.address ?? null,
|
|
82
|
+
isActive: values.isActive, emailConfirmed: values.emailConfirmed, mustChangePassword: values.mustChangePassword,
|
|
83
|
+
}));
|
|
84
|
+
await coworkee.request(`${identity}/users/${user.id}/roles`, json('PUT', { ids: values.roleIds }));
|
|
85
|
+
if (values.groupIds)
|
|
86
|
+
await coworkee.request(`${identity}/users/${user.id}/groups`, json('PUT', { ids: values.groupIds }));
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
remove: { permission: 'Identity.Users.Manage', run: (user, { request }) => request(`${identity}/users/${user.id}`, json('DELETE')), name: (user) => user.email },
|
|
90
|
+
actions: [
|
|
91
|
+
{
|
|
92
|
+
title: 'Permissions', icon: '🛡', permission: 'Identity.Permissions.Manage',
|
|
93
|
+
run: (user, coworkee) => editGrants(coworkee, 'User', user.id, user.email),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
title: 'Set password', icon: '🔑', permission: 'Identity.Users.Manage',
|
|
97
|
+
run: async (user, coworkee) => !!await formDialog(coworkee, {
|
|
98
|
+
title: `${coworkee.t('Set password')} - ${user.email}`,
|
|
99
|
+
fields: [{ name: 'password', label: 'New password', type: 'password', required: true }, { name: 'mustChangePassword', label: 'Must change password', type: 'bool' }],
|
|
100
|
+
onSubmit: (values) => coworkee.request(`${identity}/users/${user.id}/password`, json('PUT', values)),
|
|
101
|
+
}) && false,
|
|
102
|
+
},
|
|
103
|
+
{ title: 'Lock', icon: '🔒', permission: 'Identity.Users.Manage', run: (user, { request }) => request(`${identity}/users/${user.id}/lock`, json('POST', { until: null })) },
|
|
104
|
+
{ title: 'Unlock', icon: '🔓', permission: 'Identity.Users.Manage', run: (user, { request }) => request(`${identity}/users/${user.id}/unlock`, json('POST')) },
|
|
105
|
+
{
|
|
106
|
+
title: 'Send password reset', icon: '✉', permission: 'Identity.Users.Manage',
|
|
107
|
+
run: async (user, coworkee) => { await coworkee.request(`${identity}/users/${user.id}/password-reset`, json('POST')); coworkee.notify(coworkee.t('Password reset mail queued for {0}.', user.email)); return false; },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: 'Sign out everywhere', icon: '⏻', permission: 'Identity.Users.Manage',
|
|
111
|
+
run: async (user, coworkee) => { await coworkee.request(`${identity}/users/${user.id}/sign-out`, json('POST')); coworkee.notify(coworkee.t('The user was signed out everywhere.')); return false; },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
live: () => ['tenant:users'],
|
|
115
|
+
});
|
|
116
|
+
export const groupsApp = crudApp({
|
|
117
|
+
id: 'coworkee.admin.groups', group: 'Identity', title: 'Groups', icon: 'ratio-personal.png', accent: '#6a1b9a',
|
|
118
|
+
permissions: ['Identity.Groups.View'],
|
|
119
|
+
search: true, serverSearch: true,
|
|
120
|
+
load: async ({ request }, { page, pageSize, search }) => {
|
|
121
|
+
const result = await request(`${identity}/groups?${new URLSearchParams({ Page: String(page), PageSize: String(pageSize), Search: search })}`);
|
|
122
|
+
return { items: result.items, total: Number(result.totalCount) };
|
|
123
|
+
},
|
|
124
|
+
columns: [
|
|
125
|
+
{ field: 'name', title: 'Name' }, { field: 'description', title: 'Description' },
|
|
126
|
+
{ field: 'memberIds', title: 'Members', format: (group) => String(group.memberIds.length) },
|
|
127
|
+
],
|
|
128
|
+
create: {
|
|
129
|
+
permission: 'Identity.Groups.Manage',
|
|
130
|
+
fields: [{ name: 'name', label: 'Name', required: true }, { name: 'description', label: 'Description', type: 'textarea' }],
|
|
131
|
+
save: (values, { request }) => request(`${identity}/groups`, json('POST', values)),
|
|
132
|
+
},
|
|
133
|
+
edit: {
|
|
134
|
+
permission: 'Identity.Groups.Manage',
|
|
135
|
+
fields: async (coworkee) => [
|
|
136
|
+
{ name: 'name', label: 'Name', required: true }, { name: 'description', label: 'Description', type: 'textarea' },
|
|
137
|
+
{ name: 'memberIds', label: 'Members', type: 'lookup', lookup: userLookup(coworkee) },
|
|
138
|
+
{ name: 'roleIds', label: 'Roles', type: 'multi', options: await roleChoices(coworkee) },
|
|
139
|
+
],
|
|
140
|
+
save: async (group, values, { request }) => {
|
|
141
|
+
await request(`${identity}/groups/${group.id}`, json('PUT', { name: values.name, description: values.description }));
|
|
142
|
+
await request(`${identity}/groups/${group.id}/members`, json('PUT', { ids: values.memberIds }));
|
|
143
|
+
await request(`${identity}/groups/${group.id}/roles`, json('PUT', { ids: values.roleIds }));
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
remove: { permission: 'Identity.Groups.Manage', run: (group, { request }) => request(`${identity}/groups/${group.id}`, json('DELETE')) },
|
|
147
|
+
actions: [{ title: 'Permissions', icon: '🛡', permission: 'Identity.Permissions.Manage', run: (group, coworkee) => editGrants(coworkee, 'Group', group.id, group.name) }],
|
|
148
|
+
});
|
|
149
|
+
const roleFields = [
|
|
150
|
+
{ name: 'name', label: 'Name', required: true }, { name: 'description', label: 'Description', type: 'textarea' },
|
|
151
|
+
{ name: 'selectableForRegistration', label: 'Selectable for registration', type: 'bool' },
|
|
152
|
+
];
|
|
153
|
+
export const rolesApp = crudApp({
|
|
154
|
+
id: 'coworkee.admin.roles', group: 'Identity', title: 'Roles', icon: 'fn-abcAnalysis.png', accent: '#ad1457',
|
|
155
|
+
permissions: ['Identity.Roles.View'],
|
|
156
|
+
search: true,
|
|
157
|
+
load: async ({ request }) => ({ items: await request(`${identity}/roles`) }),
|
|
158
|
+
columns: [{ field: 'name', title: 'Name' }, { field: 'description', title: 'Description' }, { field: 'isSystem', title: 'System', format: (role) => check(role.isSystem), html: true }],
|
|
159
|
+
create: { permission: 'Identity.Roles.Manage', fields: roleFields, save: (values, { request }) => request(`${identity}/roles`, json('POST', values)) },
|
|
160
|
+
edit: { permission: 'Identity.Roles.Manage', fields: roleFields, when: (role) => !role.isSystem, save: (role, values, { request }) => request(`${identity}/roles/${role.id}`, json('PUT', values)) },
|
|
161
|
+
remove: { permission: 'Identity.Roles.Manage', when: (role) => !role.isSystem, run: (role, { request }) => request(`${identity}/roles/${role.id}`, json('DELETE')) },
|
|
162
|
+
actions: [{ title: 'Permissions', icon: '🛡', permission: 'Identity.Permissions.Manage', run: (role, coworkee) => editGrants(coworkee, 'Role', role.id, role.name) }],
|
|
163
|
+
});
|
|
164
|
+
const clientFields = [
|
|
165
|
+
{ name: 'clientId', label: 'Client id', required: true, hint: 'Letters, digits and . _ : - only' }, { name: 'displayName', label: 'Display name' },
|
|
166
|
+
{ name: 'clientType', label: 'Type', type: 'select', options: ['public', 'confidential'].map((value) => ({ value, label: value })) },
|
|
167
|
+
{ name: 'consentType', label: 'Consent', type: 'select', options: ['implicit', 'explicit', 'external', 'systematic'].map((value) => ({ value, label: value })) },
|
|
168
|
+
{ name: 'redirectUris', label: 'Redirect addresses', type: 'list', hint: 'Full addresses like https://app.example/callback' },
|
|
169
|
+
{ name: 'postLogoutRedirectUris', label: 'Addresses after sign-out', type: 'list', hint: 'Full addresses like https://app.example/callback' },
|
|
170
|
+
{ name: 'grantTypes', label: 'Type', type: 'list', hint: 'Pick a suggestion or type your own' },
|
|
171
|
+
{ name: 'scopes', label: 'Scopes', type: 'list', hint: 'Pick a suggestion or type your own' },
|
|
172
|
+
{ name: 'clientUri', label: 'Address' }, { name: 'logoUrl', label: 'Logo address' }, { name: 'description', label: 'Description', type: 'textarea' },
|
|
173
|
+
{ name: 'showInLauncher', label: 'Show in the app launcher', type: 'bool', hint: 'The app launcher of the sign-in server links here' },
|
|
174
|
+
];
|
|
175
|
+
const showSecret = async (coworkee, result) => {
|
|
176
|
+
if (result?.clientSecret)
|
|
177
|
+
await infoDialog(coworkee, coworkee.t('Client secret'), `<p>${esc(coworkee.t('Copy the secret now, it is not shown again:'))}</p><pre class="cw-secret">${esc(result.clientSecret)}</pre>`);
|
|
178
|
+
};
|
|
179
|
+
export const clientsApp = crudApp({
|
|
180
|
+
id: 'coworkee.admin.clients', group: 'Identity', title: 'Applications', icon: 'fn-userweblink.png', accent: '#00695c', width: 940,
|
|
181
|
+
permissions: ['Identity.Clients.Manage'], hostOnly: true,
|
|
182
|
+
search: true,
|
|
183
|
+
load: async ({ request }) => ({ items: await request(`${identity}/clients`) }),
|
|
184
|
+
columns: [
|
|
185
|
+
{ field: 'clientId', title: 'Client id' }, { field: 'displayName', title: 'Display name' }, { field: 'clientType', title: 'Type' },
|
|
186
|
+
{ field: 'managed', title: 'Service client', format: (client) => check(client.managed), html: true },
|
|
187
|
+
],
|
|
188
|
+
create: {
|
|
189
|
+
values: { clientType: 'public', consentType: 'implicit', grantTypes: ['authorization_code', 'refresh_token'], scopes: ['openid', 'profile', 'email', 'offline_access'] },
|
|
190
|
+
fields: clientFields,
|
|
191
|
+
save: async (values, coworkee) => showSecret(coworkee, await coworkee.request(`${identity}/clients`, json('POST', values))),
|
|
192
|
+
},
|
|
193
|
+
edit: {
|
|
194
|
+
fields: clientFields, when: (client) => !client.managed,
|
|
195
|
+
save: async (client, values, coworkee) => showSecret(coworkee, await coworkee.request(`${identity}/clients/${client.id}`, json('PUT', { ...client, ...values }))),
|
|
196
|
+
},
|
|
197
|
+
remove: { when: (client) => !client.managed, run: (client, { request }) => request(`${identity}/clients/${client.id}`, json('DELETE')), name: (client) => client.clientId },
|
|
198
|
+
actions: [
|
|
199
|
+
{
|
|
200
|
+
title: 'New secret', icon: '🔑', visible: (client) => client.clientType === 'confidential' && !client.managed,
|
|
201
|
+
run: async (client, coworkee) => { await showSecret(coworkee, await coworkee.request(`${identity}/clients/${client.id}/secret`, json('POST'))); return false; },
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
title: 'Duplicate', icon: '⎘', visible: (client) => !client.managed,
|
|
205
|
+
run: async (client, coworkee) => !!await formDialog(coworkee, {
|
|
206
|
+
title: coworkee.t('Duplicate'), fields: clientFields, values: { ...client, clientId: `${client.clientId}-copy` },
|
|
207
|
+
onSubmit: async (values) => showSecret(coworkee, await coworkee.request(`${identity}/clients`, json('POST', values))),
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
const scopeFields = [
|
|
213
|
+
{ name: 'name', label: 'Name', required: true }, { name: 'displayName', label: 'Display name' },
|
|
214
|
+
{ name: 'description', label: 'Description', type: 'textarea' }, { name: 'resources', label: 'Resources', type: 'list', hint: 'Audiences of the access token, e.g. https://api.example' },
|
|
215
|
+
];
|
|
216
|
+
export const scopesApp = crudApp({
|
|
217
|
+
id: 'coworkee.admin.scopes', group: 'Identity', title: 'Scopes', icon: 'icon-percent.png', accent: '#4e342e',
|
|
218
|
+
permissions: ['Identity.Clients.Manage'], hostOnly: true,
|
|
219
|
+
search: true,
|
|
220
|
+
load: async ({ request }) => ({ items: await request(`${identity}/scopes`) }),
|
|
221
|
+
columns: [
|
|
222
|
+
{ field: 'name', title: 'Name' }, { field: 'displayName', title: 'Display name' }, { field: 'resources', title: 'Resources', format: (scope) => scope.resources.join(', ') },
|
|
223
|
+
{ field: 'builtIn', title: 'built-in', format: (scope) => check(scope.builtIn), html: true },
|
|
224
|
+
],
|
|
225
|
+
create: { fields: scopeFields, save: (values, { request }) => request(`${identity}/scopes`, json('POST', values)) },
|
|
226
|
+
edit: { fields: scopeFields, when: (scope) => !scope.builtIn, save: (scope, values, { request }) => request(`${identity}/scopes/${scope.id}`, json('PUT', values)) },
|
|
227
|
+
remove: { when: (scope) => !scope.builtIn, run: (scope, { request }) => request(`${identity}/scopes/${scope.id}`, json('DELETE')) },
|
|
228
|
+
});
|
|
229
|
+
const inDays = (days) => new Date(Date.now() + days * 86400000).toISOString();
|
|
230
|
+
export const apiKeysApp = crudApp({
|
|
231
|
+
id: 'coworkee.admin.api-keys', group: 'Identity', title: 'API keys', icon: 'login-password.png', accent: '#37474f', width: 900,
|
|
232
|
+
permissions: ['ApiKeys.Manage'],
|
|
233
|
+
load: async ({ request }) => ({ items: await request(`${identity}/api-keys`) }),
|
|
234
|
+
columns: [
|
|
235
|
+
{ field: 'name', title: 'Name' }, { field: 'prefix', title: 'Key' }, { field: 'scopes', title: 'Scopes', format: (key) => key.scopes.join(', ') },
|
|
236
|
+
{ field: 'expiresAt', title: 'Expires' }, { field: 'lastUsedAt', title: 'Last used' },
|
|
237
|
+
{ field: 'revokedAt', title: 'Revoked', format: (key, { culture }) => display(key.revokedAt, culture) },
|
|
238
|
+
],
|
|
239
|
+
create: {
|
|
240
|
+
values: { expiresAt: inDays(90) },
|
|
241
|
+
fields: async ({ request, t }) => {
|
|
242
|
+
const groups = await request(`${identity}/api-keys/scopes`);
|
|
243
|
+
return [
|
|
244
|
+
{ name: 'name', label: 'Name', required: true }, { name: 'description', label: 'Description', type: 'textarea' },
|
|
245
|
+
{ name: 'expiresAt', label: 'Expires', type: 'date', required: true },
|
|
246
|
+
{ name: 'scopes', label: 'Scopes', type: 'multi', options: groups.flatMap((group) => group.permissions.map((p) => ({ value: p.name, label: `${t(group.displayName)}: ${t(p.displayName)}` }))) },
|
|
247
|
+
];
|
|
248
|
+
},
|
|
249
|
+
save: async (values, coworkee) => {
|
|
250
|
+
const created = await coworkee.request(`${identity}/api-keys`, json('POST', values));
|
|
251
|
+
void infoDialog(coworkee, coworkee.t('API key'), `<p>${esc(coworkee.t('Copy the key now: it is shown only this once.'))}</p><pre class="cw-secret">${esc(created.secret)}</pre>`);
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
actions: [{ title: 'Revoke', icon: '⛔', visible: (key) => !key.revokedAt, run: (key, { request }) => request(`${identity}/api-keys/${key.id}/revoke`, json('POST')) }],
|
|
255
|
+
});
|
|
256
|
+
export const identityApps = [usersApp, groupsApp, rolesApp, clientsApp, scopesApp, apiKeysApp];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AppFactory, Coworkee, CoworkeeApp } from '../../coworkee.ts';
|
|
2
|
+
export declare const administrationAppId = "coworkee.administration";
|
|
3
|
+
/** Every administration app the user may open, grouped like the administration menu of the web app. */
|
|
4
|
+
export declare const administrationApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
5
|
+
/** The administration: users, groups, roles, applications, scopes, API keys, themes, languages, translations, mail, backups, audit and more. */
|
|
6
|
+
export declare const adminApps: AppFactory[];
|
|
7
|
+
export * from './identity.ts';
|
|
8
|
+
export * from './mail.ts';
|
|
9
|
+
export * from './system.ts';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { esc } from "../../dom.js";
|
|
2
|
+
import { aiSettingsApp } from "../assistant.js";
|
|
3
|
+
import { identityApps } from "./identity.js";
|
|
4
|
+
import { mailApps } from "./mail.js";
|
|
5
|
+
import { systemApps } from "./system.js";
|
|
6
|
+
export const administrationAppId = 'coworkee.administration';
|
|
7
|
+
const sections = ['Identity', 'System', 'Communication', 'Localization', 'Monitoring'];
|
|
8
|
+
/** No user holds it: the administration app stays out of sight while none of its apps is allowed. */
|
|
9
|
+
const nobody = 'Coworkee.Administration.None';
|
|
10
|
+
const allowed = (coworkee) => coworkee.apps.filter((app) => app.group && !app.hidden && (app.requiredPermissions ?? []).every(coworkee.can));
|
|
11
|
+
/** Every administration app the user may open, grouped like the administration menu of the web app. */
|
|
12
|
+
export const administrationApp = (coworkee) => {
|
|
13
|
+
const definition = (visible) => ({
|
|
14
|
+
id: administrationAppId, title: coworkee.t('Administration'), icon: 'startmenu-settings.png', accent: '#35496b', width: 760, height: 560, singleInstance: true,
|
|
15
|
+
requiredPermissions: visible ? [] : [nobody],
|
|
16
|
+
render: ({ host }) => {
|
|
17
|
+
const { t } = coworkee;
|
|
18
|
+
const apps = allowed(coworkee);
|
|
19
|
+
host.classList.add('cw-host');
|
|
20
|
+
host.innerHTML = `<div class="cw-app cw-admin">${[...sections, ...new Set(apps.map((app) => app.group))].filter((section, index, all) => all.indexOf(section) === index)
|
|
21
|
+
.map((section) => [section, apps.filter((app) => app.group === section)]).filter(([, items]) => items.length > 0)
|
|
22
|
+
.map(([section, items]) => `<section><h3>${esc(t(section))}</h3><div class="cw-tiles">${items.map((app) => `<button data-open="${esc(app.id)}" style="--cw-tile:${esc(app.accent ?? '#35496b')}"><img src="${esc(/[/:]/.test(app.icon) ? app.icon : `/icons/${app.icon}`)}" alt=""/><span>${esc(app.title)}</span></button>`).join('')}</div></section>`).join('')
|
|
23
|
+
|| `<p class="cw-muted">${esc(t('Nothing found.'))}</p>`}</div>`;
|
|
24
|
+
host.addEventListener('click', (event) => {
|
|
25
|
+
const id = event.target.closest('[data-open]')?.dataset.open;
|
|
26
|
+
if (id)
|
|
27
|
+
coworkee.airWorks.openApp(id);
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// hidden until the access of the signed-in user is known; every change of the access decides again
|
|
32
|
+
let visible = false;
|
|
33
|
+
const update = () => {
|
|
34
|
+
const now = allowed(coworkee).length > 0;
|
|
35
|
+
if (now === visible)
|
|
36
|
+
return;
|
|
37
|
+
visible = now;
|
|
38
|
+
coworkee.airWorks.unregisterApp(administrationAppId);
|
|
39
|
+
coworkee.airWorks.registerApp(definition(now));
|
|
40
|
+
};
|
|
41
|
+
coworkee.airWorks.on('access-changed', update);
|
|
42
|
+
return definition(false);
|
|
43
|
+
};
|
|
44
|
+
/** The administration: users, groups, roles, applications, scopes, API keys, themes, languages, translations, mail, backups, audit and more. */
|
|
45
|
+
export const adminApps = [administrationApp, ...identityApps, ...systemApps, ...mailApps, aiSettingsApp];
|
|
46
|
+
export * from "./identity.js";
|
|
47
|
+
export * from "./mail.js";
|
|
48
|
+
export * from "./system.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Schemas } from '@coworkee/client';
|
|
2
|
+
import type { Coworkee, CoworkeeApp } from '../../coworkee.ts';
|
|
3
|
+
import { type Values } from '../../form.ts';
|
|
4
|
+
type Source = Schemas['RecipientSourceDto'];
|
|
5
|
+
export declare const mailTemplateAppId = "coworkee.admin.mail-template";
|
|
6
|
+
export declare const mailLogAppId = "coworkee.admin.mail-log";
|
|
7
|
+
/** The recipient source the API takes from the form values: only the fields of the chosen kind. */
|
|
8
|
+
export declare const sourceOf: (values: Values) => Source;
|
|
9
|
+
/** How many users a source reaches, without those who unsubscribed, and the first of them. */
|
|
10
|
+
export declare function previewRecipients(coworkee: Coworkee, source: Source): Promise<boolean>;
|
|
11
|
+
export declare const mailTemplatesApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
12
|
+
/** Subject and body of a template in one language with a live preview, a test mail to oneself and the way back to the default. */
|
|
13
|
+
export declare const mailTemplateApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
14
|
+
export declare const mailCampaignsApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
15
|
+
export declare const recipientListsApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
16
|
+
/** The mails the app sent or queued; opened from a campaign it shows that campaign's mails. */
|
|
17
|
+
export declare const mailLogApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
18
|
+
export declare const mailSettingsApp: (coworkee: Coworkee) => CoworkeeApp;
|
|
19
|
+
export declare const mailApps: ((coworkee: Coworkee) => CoworkeeApp)[];
|
|
20
|
+
export {};
|