@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/api-services",
3
- "version": "26.8.52",
3
+ "version": "26.8.54",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 getUsersList = async (params: ApiParams) => {
51
- const fieldsToSend = [
52
- 'page',
53
- 'size',
54
- 'q',
55
- 'sort',
56
- 'fields',
57
- 'id',
58
- 'not_user_id',
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
- sanitize(fieldsToSend),
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
 
@@ -0,0 +1,9 @@
1
+ export const UserPresenceStatus = {
2
+ WEB: 'web',
3
+ SIP: 'sip',
4
+ DLG: 'dlg',
5
+ DND: 'dnd',
6
+ } as const;
7
+
8
+ export type UserPresenceStatus =
9
+ (typeof UserPresenceStatus)[keyof typeof UserPresenceStatus];
@@ -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
  };
@@ -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;