@webitel/api-services 26.8.52 → 26.8.54
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/package.json +1 -1
- package/src/api/clients/configurations/configurations.ts +25 -0
- package/src/api/clients/users/users.ts +37 -11
- package/src/enums/UserPresenceStatus/UserPresenceStatus.ts +9 -0
- package/src/enums/index.ts +2 -0
- package/src/validations/index.ts +1 -0
- package/src/validations/user/__tests__/user.validations.spec.ts +136 -0
- package/src/validations/user/user.validations.ts +97 -0
- package/types/.tsbuildinfo +1 -1
- package/types/api/clients/configurations/configurations.d.ts +2 -0
- package/types/api/clients/users/users.d.ts +1 -0
- package/types/enums/UserPresenceStatus/UserPresenceStatus.d.ts +7 -0
- package/types/enums/index.d.ts +2 -1
- package/types/validations/index.d.ts +1 -0
- package/types/validations/user/user.validations.d.ts +104 -0
package/package.json
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EngineSystemSetting,
|
|
3
|
+
EngineSystemSettingName,
|
|
4
|
+
} from '@webitel/api-services/gen/models';
|
|
1
5
|
import { getShallowFieldsToSendFromZodSchema } from '@webitel/api-services/gen/utils';
|
|
6
|
+
import type { UserPasswordRules } from '@webitel/api-services/validations';
|
|
2
7
|
import {
|
|
3
8
|
CreateSystemSettingBody,
|
|
4
9
|
getSystemSettingService,
|
|
@@ -182,6 +187,25 @@ const getObjectsList = async (params: ApiParams) => {
|
|
|
182
187
|
}
|
|
183
188
|
};
|
|
184
189
|
|
|
190
|
+
const getPasswordRules = async (): Promise<UserPasswordRules> => {
|
|
191
|
+
const { items } = await getList({
|
|
192
|
+
name: [
|
|
193
|
+
EngineSystemSettingName.PasswordRegExp,
|
|
194
|
+
EngineSystemSettingName.PasswordValidationText,
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const findValue = (name: EngineSystemSettingName) =>
|
|
199
|
+
items.find((item: EngineSystemSetting) => item.name === name)?.value;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
passwordRegExp: findValue(EngineSystemSettingName.PasswordRegExp),
|
|
203
|
+
passwordValidationText: findValue(
|
|
204
|
+
EngineSystemSettingName.PasswordValidationText,
|
|
205
|
+
),
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
185
209
|
export const ConfigurationsAPI = {
|
|
186
210
|
getList,
|
|
187
211
|
get,
|
|
@@ -190,4 +214,5 @@ export const ConfigurationsAPI = {
|
|
|
190
214
|
delete: deleteItem,
|
|
191
215
|
getLookup,
|
|
192
216
|
getObjectsList,
|
|
217
|
+
getPasswordRules,
|
|
193
218
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { getShallowFieldsToSendFromZodSchema } from '@webitel/api-services/gen/utils';
|
|
1
2
|
import deepCopy from 'deep-copy';
|
|
3
|
+
|
|
4
|
+
import { SearchUsersQueryParams } from '../../../gen-wire';
|
|
2
5
|
import {
|
|
3
6
|
getDefaultGetListResponse,
|
|
4
7
|
getDefaultGetParams,
|
|
@@ -13,6 +16,7 @@ import {
|
|
|
13
16
|
mergeEach,
|
|
14
17
|
notify,
|
|
15
18
|
sanitize,
|
|
19
|
+
sanitizeToWire,
|
|
16
20
|
snakeToCamel,
|
|
17
21
|
} from '../../transformers';
|
|
18
22
|
import { generatePermissionsApi } from '../_shared/generatePermissionsApi';
|
|
@@ -47,17 +51,25 @@ const fieldsToSend = [
|
|
|
47
51
|
'forcePasswordChange',
|
|
48
52
|
];
|
|
49
53
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
const presenceListFields = [
|
|
55
|
+
'presence.status',
|
|
56
|
+
'dnd',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const toWireListFields = (fields?: string[]) =>
|
|
60
|
+
fields && [
|
|
61
|
+
...new Set(
|
|
62
|
+
fields.map((field) =>
|
|
63
|
+
presenceListFields.includes(field) ? 'presence' : field,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
59
66
|
];
|
|
60
67
|
|
|
68
|
+
const getUsersList = async (params: ApiParams) => {
|
|
69
|
+
const listFieldsToSend = getShallowFieldsToSendFromZodSchema(
|
|
70
|
+
SearchUsersQueryParams,
|
|
71
|
+
);
|
|
72
|
+
|
|
61
73
|
const defaultObject = {
|
|
62
74
|
name: '',
|
|
63
75
|
status: '',
|
|
@@ -69,9 +81,10 @@ const getUsersList = async (params: ApiParams) => {
|
|
|
69
81
|
merge(getDefaultGetParams()),
|
|
70
82
|
(params) => ({
|
|
71
83
|
...params,
|
|
72
|
-
q: params.search,
|
|
84
|
+
q: params.q ?? params.search,
|
|
85
|
+
fields: toWireListFields(params.fields),
|
|
73
86
|
}),
|
|
74
|
-
|
|
87
|
+
sanitizeToWire(listFieldsToSend),
|
|
75
88
|
camelToSnake(),
|
|
76
89
|
generateUrl(baseUrl),
|
|
77
90
|
]);
|
|
@@ -308,6 +321,18 @@ const getUsersLookup = (params: Parameters<typeof getUsersList>[0]) =>
|
|
|
308
321
|
],
|
|
309
322
|
});
|
|
310
323
|
|
|
324
|
+
const generateUserTotpUrl = async ({ id }: DeleteItemParams) => {
|
|
325
|
+
const url = `${baseUrl}/${id}/2fa`;
|
|
326
|
+
try {
|
|
327
|
+
const response = await instance.post(url, {});
|
|
328
|
+
return applyTransform(response.data, []);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
throw applyTransform(err, [
|
|
331
|
+
notify,
|
|
332
|
+
]);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
311
336
|
const logoutUser = async ({ id }: DeleteItemParams) => {
|
|
312
337
|
const url = `${baseUrl}/${id}/logout`;
|
|
313
338
|
try {
|
|
@@ -343,6 +368,7 @@ export const UsersAPI = {
|
|
|
343
368
|
delete: deleteUser,
|
|
344
369
|
getLookup: getUsersLookup,
|
|
345
370
|
patchUserPresence,
|
|
371
|
+
generateTotpUrl: generateUserTotpUrl,
|
|
346
372
|
logoutUser,
|
|
347
373
|
logoutMultipleUsers,
|
|
348
374
|
|
package/src/enums/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { QueueType, QueueTypeName } from './QueueType/QueueType';
|
|
|
10
10
|
import { RelativeDatetimeValue } from './RelativeDatetimeValue/RelativeDatetimeValue';
|
|
11
11
|
import { TimeBaseScore } from './TimeBaseScore/TimeBaseScore';
|
|
12
12
|
import { TypesResourceStrategy } from './TypesResourceStrategy/TypesResourceStrategy';
|
|
13
|
+
import { UserPresenceStatus } from './UserPresenceStatus/UserPresenceStatus';
|
|
13
14
|
|
|
14
15
|
export {
|
|
15
16
|
CallReportingStatus,
|
|
@@ -23,4 +24,5 @@ export {
|
|
|
23
24
|
RelativeDatetimeValue,
|
|
24
25
|
TimeBaseScore,
|
|
25
26
|
TypesResourceStrategy,
|
|
27
|
+
UserPresenceStatus,
|
|
26
28
|
};
|
package/src/validations/index.ts
CHANGED
|
@@ -44,4 +44,5 @@ export * from './sla/sla.validations';
|
|
|
44
44
|
export * from './slaCondition/slaCondition.validations';
|
|
45
45
|
export * from './typeExtension/typeExtension.validations';
|
|
46
46
|
export * from './types';
|
|
47
|
+
export * from './user/user.validations';
|
|
47
48
|
export * from './variable/variable.validations';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getDefaultsFromZodSchema } from '@webitel/api-services/gen/utils';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { buildUserSchema, userSchema } from '../user.validations';
|
|
5
|
+
|
|
6
|
+
const issuePaths = (result: ReturnType<typeof userSchema.safeParse>) =>
|
|
7
|
+
result.error?.issues.map(({ path }) => path.join('.')) ?? [];
|
|
8
|
+
|
|
9
|
+
describe('userSchema', () => {
|
|
10
|
+
it('seeds a new user with the form defaults', () => {
|
|
11
|
+
expect(getDefaultsFromZodSchema(userSchema, {})).toMatchObject({
|
|
12
|
+
name: '',
|
|
13
|
+
username: '',
|
|
14
|
+
password: '',
|
|
15
|
+
extension: '',
|
|
16
|
+
roles: [],
|
|
17
|
+
license: [],
|
|
18
|
+
devices: [],
|
|
19
|
+
device: {},
|
|
20
|
+
variables: [],
|
|
21
|
+
forcePasswordChange: false,
|
|
22
|
+
generateDevice: false,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('requires name, login and password for a new user', () => {
|
|
27
|
+
const paths = issuePaths(
|
|
28
|
+
userSchema.safeParse(getDefaultsFromZodSchema(userSchema, {})),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(paths).toEqual(
|
|
32
|
+
expect.arrayContaining([
|
|
33
|
+
'name',
|
|
34
|
+
'username',
|
|
35
|
+
'password',
|
|
36
|
+
]),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does not require password for an existing user', () => {
|
|
41
|
+
const result = userSchema.safeParse({
|
|
42
|
+
id: '1',
|
|
43
|
+
name: 'John',
|
|
44
|
+
username: 'john',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.success).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('accepts digits-only extension', () => {
|
|
51
|
+
expect(
|
|
52
|
+
issuePaths(
|
|
53
|
+
userSchema.safeParse({
|
|
54
|
+
id: '1',
|
|
55
|
+
name: 'John',
|
|
56
|
+
username: 'john',
|
|
57
|
+
extension: '12a',
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
).toEqual([
|
|
61
|
+
'extension',
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('requires the other half of a filled variable pair', () => {
|
|
66
|
+
const paths = issuePaths(
|
|
67
|
+
userSchema.safeParse({
|
|
68
|
+
id: '1',
|
|
69
|
+
name: 'John',
|
|
70
|
+
username: 'john',
|
|
71
|
+
variables: [
|
|
72
|
+
{
|
|
73
|
+
key: 'k',
|
|
74
|
+
value: '',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: '',
|
|
78
|
+
value: 'v',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: '',
|
|
82
|
+
value: '',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(paths).toEqual([
|
|
89
|
+
'variables.0.value',
|
|
90
|
+
'variables.1.key',
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('buildUserSchema', () => {
|
|
96
|
+
const user = {
|
|
97
|
+
id: '1',
|
|
98
|
+
name: 'John',
|
|
99
|
+
username: 'john',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
it('checks a filled password against the configured regexp', () => {
|
|
103
|
+
const schema = buildUserSchema({
|
|
104
|
+
passwordRegExp: '^\\d+$',
|
|
105
|
+
passwordValidationText: 'Digits only',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const result = schema.safeParse({
|
|
109
|
+
...user,
|
|
110
|
+
password: 'abc',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(result.error?.issues).toEqual([
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
path: [
|
|
116
|
+
'password',
|
|
117
|
+
],
|
|
118
|
+
message: 'Digits only',
|
|
119
|
+
}),
|
|
120
|
+
]);
|
|
121
|
+
expect(
|
|
122
|
+
schema.safeParse({
|
|
123
|
+
...user,
|
|
124
|
+
password: '123',
|
|
125
|
+
}).success,
|
|
126
|
+
).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('skips the regexp for an empty password on an existing user', () => {
|
|
130
|
+
const schema = buildUserSchema({
|
|
131
|
+
passwordRegExp: '^\\d+$',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(schema.safeParse(user).success).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ApiUser } from '@webitel/api-services/gen/models';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { i18nIssue } from '../_shared/i18nIssue';
|
|
5
|
+
import { flexibleLookupSchema } from '../_shared/lookup.validations';
|
|
6
|
+
import { variablePairSchema } from '../_shared/variablePair.validations';
|
|
7
|
+
|
|
8
|
+
const digitsRegex = /^[0-9]*$/;
|
|
9
|
+
|
|
10
|
+
export const userSchemaBase = z.object({
|
|
11
|
+
id: z.string().optional(),
|
|
12
|
+
name: z.string().min(1).default(''),
|
|
13
|
+
username: z.string().min(1).default(''),
|
|
14
|
+
password: z.string().optional().default(''),
|
|
15
|
+
extension: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.default('')
|
|
19
|
+
.refine((value) => !value || digitsRegex.test(value), i18nIssue('numeric')),
|
|
20
|
+
email: z.string().optional().default(''),
|
|
21
|
+
chatName: z.string().optional().default(''),
|
|
22
|
+
contact: flexibleLookupSchema.nullish().default({}),
|
|
23
|
+
roles: z.array(flexibleLookupSchema).optional().default([]),
|
|
24
|
+
license: z.array(z.any()).optional().default([]),
|
|
25
|
+
devices: z.array(flexibleLookupSchema).optional().default([]),
|
|
26
|
+
device: flexibleLookupSchema.nullish().default({}),
|
|
27
|
+
forcePasswordChange: z.boolean().optional().default(false),
|
|
28
|
+
generateDevice: z.boolean().optional().default(false),
|
|
29
|
+
variables: z.array(variablePairSchema).optional().default([]),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const userSchema = userSchemaBase.superRefine((user, ctx) => {
|
|
33
|
+
if (!user.id && !user.password) {
|
|
34
|
+
ctx.addIssue({
|
|
35
|
+
code: 'custom',
|
|
36
|
+
path: [
|
|
37
|
+
'password',
|
|
38
|
+
],
|
|
39
|
+
...i18nIssue('required'),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
user.variables?.forEach(({ key, value }, index) => {
|
|
44
|
+
if (value && !key) {
|
|
45
|
+
ctx.addIssue({
|
|
46
|
+
code: 'custom',
|
|
47
|
+
path: [
|
|
48
|
+
'variables',
|
|
49
|
+
index,
|
|
50
|
+
'key',
|
|
51
|
+
],
|
|
52
|
+
...i18nIssue('required'),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (key && !value) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: 'custom',
|
|
58
|
+
path: [
|
|
59
|
+
'variables',
|
|
60
|
+
index,
|
|
61
|
+
'value',
|
|
62
|
+
],
|
|
63
|
+
...i18nIssue('required'),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export interface UserPasswordRules {
|
|
70
|
+
passwordRegExp?: string;
|
|
71
|
+
passwordValidationText?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const buildUserSchema = ({
|
|
75
|
+
passwordRegExp,
|
|
76
|
+
passwordValidationText,
|
|
77
|
+
}: UserPasswordRules = {}) =>
|
|
78
|
+
userSchema.superRefine((user, ctx) => {
|
|
79
|
+
if (!passwordRegExp || !user.password) return;
|
|
80
|
+
if (new RegExp(passwordRegExp).test(user.password)) return;
|
|
81
|
+
|
|
82
|
+
ctx.addIssue({
|
|
83
|
+
code: 'custom',
|
|
84
|
+
path: [
|
|
85
|
+
'password',
|
|
86
|
+
],
|
|
87
|
+
...(passwordValidationText
|
|
88
|
+
? {
|
|
89
|
+
message: passwordValidationText,
|
|
90
|
+
}
|
|
91
|
+
: i18nIssue('isRegExpMatched')),
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export type UserFormShape = z.input<typeof userSchemaBase>;
|
|
96
|
+
|
|
97
|
+
export type UserCard = Omit<ApiUser, keyof UserFormShape> & UserFormShape;
|