@nocobase/plugin-acl 0.7.4-alpha.4 → 0.7.5-alpha.1
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/lib/server.js +2 -1
- package/package.json +6 -6
- package/src/__tests__/acl.test.ts +142 -154
- package/src/__tests__/prepare.ts +2 -3
- package/src/__tests__/role.test.ts +15 -13
- package/src/__tests__/users.test.ts +52 -0
- package/src/server.ts +7 -5
package/lib/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/plugin-acl",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5-alpha.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"licenses": [
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
"main": "./lib/index.js",
|
|
13
13
|
"types": "./lib/index.d.ts",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@nocobase/acl": "0.7.
|
|
16
|
-
"@nocobase/database": "0.7.
|
|
17
|
-
"@nocobase/plugin-users": "0.7.
|
|
18
|
-
"@nocobase/server": "0.7.
|
|
15
|
+
"@nocobase/acl": "0.7.5-alpha.1",
|
|
16
|
+
"@nocobase/database": "0.7.5-alpha.1",
|
|
17
|
+
"@nocobase/plugin-users": "0.7.5-alpha.1",
|
|
18
|
+
"@nocobase/server": "0.7.5-alpha.1"
|
|
19
19
|
},
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
22
|
"url": "git+https://github.com/nocobase/nocobase.git",
|
|
23
23
|
"directory": "packages/plugins/acl"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "f6eb27b68185bb0c0b4c2cfca1df84205a9b9173"
|
|
26
26
|
}
|
|
@@ -26,14 +26,18 @@ describe('acl', () => {
|
|
|
26
26
|
const UserRepo = db.getCollection('users').repository;
|
|
27
27
|
admin = await UserRepo.create({
|
|
28
28
|
values: {
|
|
29
|
-
roles: ['admin']
|
|
30
|
-
}
|
|
29
|
+
roles: ['admin'],
|
|
30
|
+
},
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
|
|
35
|
+
adminAgent = app.agent().auth(
|
|
36
|
+
userPlugin.jwtService.sign({
|
|
37
|
+
userId: admin.get('id'),
|
|
38
|
+
}),
|
|
39
|
+
{ type: 'bearer' },
|
|
40
|
+
);
|
|
37
41
|
|
|
38
42
|
uiSchemaRepository = db.getRepository('uiSchemas');
|
|
39
43
|
});
|
|
@@ -41,7 +45,7 @@ describe('acl', () => {
|
|
|
41
45
|
it('should works with universal actions', async () => {
|
|
42
46
|
await db.getRepository('roles').create({
|
|
43
47
|
values: {
|
|
44
|
-
name: 'new'
|
|
48
|
+
name: 'new',
|
|
45
49
|
},
|
|
46
50
|
});
|
|
47
51
|
|
|
@@ -54,16 +58,15 @@ describe('acl', () => {
|
|
|
54
58
|
).toBeNull();
|
|
55
59
|
|
|
56
60
|
// grant universal action
|
|
57
|
-
await adminAgent
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
strategy: {
|
|
63
|
-
actions: ['create'],
|
|
64
|
-
},
|
|
61
|
+
await adminAgent.resource('roles').update({
|
|
62
|
+
resourceIndex: 'new',
|
|
63
|
+
values: {
|
|
64
|
+
strategy: {
|
|
65
|
+
actions: ['create'],
|
|
65
66
|
},
|
|
66
|
-
}
|
|
67
|
+
},
|
|
68
|
+
forceUpdate: true,
|
|
69
|
+
});
|
|
67
70
|
|
|
68
71
|
expect(
|
|
69
72
|
acl.can({
|
|
@@ -104,15 +107,13 @@ describe('acl', () => {
|
|
|
104
107
|
},
|
|
105
108
|
});
|
|
106
109
|
|
|
107
|
-
await adminAgent
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
},
|
|
115
|
-
});
|
|
110
|
+
await adminAgent.resource('roles.resources', 'new').create({
|
|
111
|
+
values: {
|
|
112
|
+
name: 'c1',
|
|
113
|
+
usingActionsConfig: true,
|
|
114
|
+
actions: [],
|
|
115
|
+
},
|
|
116
|
+
});
|
|
116
117
|
|
|
117
118
|
expect(
|
|
118
119
|
acl.can({
|
|
@@ -150,39 +151,37 @@ describe('acl', () => {
|
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
// create c1 published scope
|
|
153
|
-
const {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
},
|
|
154
|
+
const {
|
|
155
|
+
body: { data: publishedScope },
|
|
156
|
+
} = await adminAgent.resource('rolesResourcesScopes').create({
|
|
157
|
+
values: {
|
|
158
|
+
resourceName: 'c1',
|
|
159
|
+
name: 'published',
|
|
160
|
+
scope: {
|
|
161
|
+
published: true,
|
|
162
162
|
},
|
|
163
|
-
}
|
|
163
|
+
},
|
|
164
|
+
});
|
|
164
165
|
|
|
165
166
|
// await db.getRepository('rolesResourcesScopes').findOne();
|
|
166
167
|
|
|
167
168
|
// set admin resources
|
|
168
|
-
await adminAgent
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
},
|
|
185
|
-
});
|
|
169
|
+
await adminAgent.resource('roles.resources', 'new').create({
|
|
170
|
+
values: {
|
|
171
|
+
name: 'c1',
|
|
172
|
+
usingActionsConfig: true,
|
|
173
|
+
actions: [
|
|
174
|
+
{
|
|
175
|
+
name: 'create',
|
|
176
|
+
scope: publishedScope.id,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'view',
|
|
180
|
+
fields: ['title', 'age'],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
});
|
|
186
185
|
|
|
187
186
|
expect(
|
|
188
187
|
acl.can({
|
|
@@ -215,32 +214,28 @@ describe('acl', () => {
|
|
|
215
214
|
});
|
|
216
215
|
|
|
217
216
|
// revoke action
|
|
218
|
-
const response = await adminAgent
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
appends: ['actions'],
|
|
222
|
-
});
|
|
217
|
+
const response = await adminAgent.resource('roles.resources', role.get('name')).list({
|
|
218
|
+
appends: ['actions'],
|
|
219
|
+
});
|
|
223
220
|
|
|
224
221
|
expect(response.statusCode).toEqual(200);
|
|
225
222
|
|
|
226
223
|
const actions = response.body.data[0].actions;
|
|
227
224
|
const collectionName = response.body.data[0].name;
|
|
228
225
|
|
|
229
|
-
await adminAgent
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
},
|
|
243
|
-
});
|
|
226
|
+
await adminAgent.resource('roles.resources', role.get('name')).update({
|
|
227
|
+
filterByTk: collectionName,
|
|
228
|
+
values: {
|
|
229
|
+
name: 'c1',
|
|
230
|
+
usingActionsConfig: true,
|
|
231
|
+
actions: [
|
|
232
|
+
{
|
|
233
|
+
name: 'view',
|
|
234
|
+
fields: ['title', 'age'],
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
});
|
|
244
239
|
|
|
245
240
|
expect(
|
|
246
241
|
acl.can({
|
|
@@ -254,7 +249,7 @@ describe('acl', () => {
|
|
|
254
249
|
it('should revoke resource when collection destroy', async () => {
|
|
255
250
|
await db.getRepository('roles').create({
|
|
256
251
|
values: {
|
|
257
|
-
name: 'new'
|
|
252
|
+
name: 'new',
|
|
258
253
|
},
|
|
259
254
|
});
|
|
260
255
|
|
|
@@ -272,21 +267,19 @@ describe('acl', () => {
|
|
|
272
267
|
},
|
|
273
268
|
});
|
|
274
269
|
|
|
275
|
-
await adminAgent
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
},
|
|
289
|
-
});
|
|
270
|
+
await adminAgent.resource('roles.resources').create({
|
|
271
|
+
associatedIndex: 'new',
|
|
272
|
+
values: {
|
|
273
|
+
name: 'posts',
|
|
274
|
+
usingActionsConfig: true,
|
|
275
|
+
actions: [
|
|
276
|
+
{
|
|
277
|
+
name: 'view',
|
|
278
|
+
fields: ['title'],
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
});
|
|
290
283
|
|
|
291
284
|
expect(
|
|
292
285
|
acl.can({
|
|
@@ -314,7 +307,7 @@ describe('acl', () => {
|
|
|
314
307
|
it('should revoke actions when not using actions config', async () => {
|
|
315
308
|
await db.getRepository('roles').create({
|
|
316
309
|
values: {
|
|
317
|
-
name: 'new'
|
|
310
|
+
name: 'new',
|
|
318
311
|
},
|
|
319
312
|
});
|
|
320
313
|
|
|
@@ -325,20 +318,18 @@ describe('acl', () => {
|
|
|
325
318
|
},
|
|
326
319
|
});
|
|
327
320
|
|
|
328
|
-
await adminAgent
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
},
|
|
341
|
-
});
|
|
321
|
+
await adminAgent.resource('roles.resources').create({
|
|
322
|
+
associatedIndex: 'new',
|
|
323
|
+
values: {
|
|
324
|
+
name: 'posts',
|
|
325
|
+
usingActionsConfig: true,
|
|
326
|
+
actions: [
|
|
327
|
+
{
|
|
328
|
+
name: 'create',
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
});
|
|
342
333
|
|
|
343
334
|
expect(
|
|
344
335
|
acl.can({
|
|
@@ -352,21 +343,19 @@ describe('acl', () => {
|
|
|
352
343
|
action: 'create',
|
|
353
344
|
});
|
|
354
345
|
|
|
355
|
-
await adminAgent
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
},
|
|
369
|
-
});
|
|
346
|
+
await adminAgent.resource('roles.resources', 'new').update({
|
|
347
|
+
filterByTk: (
|
|
348
|
+
await db.getRepository('rolesResources').findOne({
|
|
349
|
+
filter: {
|
|
350
|
+
name: 'posts',
|
|
351
|
+
roleName: 'new',
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
).get('name') as string,
|
|
355
|
+
values: {
|
|
356
|
+
usingActionsConfig: false,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
370
359
|
|
|
371
360
|
expect(
|
|
372
361
|
acl.can({
|
|
@@ -376,21 +365,19 @@ describe('acl', () => {
|
|
|
376
365
|
}),
|
|
377
366
|
).toBeNull();
|
|
378
367
|
|
|
379
|
-
await adminAgent
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
},
|
|
393
|
-
});
|
|
368
|
+
await adminAgent.resource('roles.resources', 'new').update({
|
|
369
|
+
filterByTk: (
|
|
370
|
+
await db.getRepository('rolesResources').findOne({
|
|
371
|
+
filter: {
|
|
372
|
+
name: 'posts',
|
|
373
|
+
roleName: 'new',
|
|
374
|
+
},
|
|
375
|
+
})
|
|
376
|
+
).get('name') as string,
|
|
377
|
+
values: {
|
|
378
|
+
usingActionsConfig: true,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
394
381
|
|
|
395
382
|
expect(
|
|
396
383
|
acl.can({
|
|
@@ -408,7 +395,7 @@ describe('acl', () => {
|
|
|
408
395
|
it('should add fields when field created', async () => {
|
|
409
396
|
await db.getRepository('roles').create({
|
|
410
397
|
values: {
|
|
411
|
-
name: 'new'
|
|
398
|
+
name: 'new',
|
|
412
399
|
},
|
|
413
400
|
});
|
|
414
401
|
|
|
@@ -426,21 +413,19 @@ describe('acl', () => {
|
|
|
426
413
|
},
|
|
427
414
|
});
|
|
428
415
|
|
|
429
|
-
await adminAgent
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
},
|
|
443
|
-
});
|
|
416
|
+
await adminAgent.resource('roles.resources').create({
|
|
417
|
+
associatedIndex: 'new',
|
|
418
|
+
values: {
|
|
419
|
+
name: 'posts',
|
|
420
|
+
usingActionsConfig: true,
|
|
421
|
+
actions: [
|
|
422
|
+
{
|
|
423
|
+
name: 'view',
|
|
424
|
+
fields: ['title'],
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
});
|
|
444
429
|
|
|
445
430
|
const allowFields = acl.can({
|
|
446
431
|
role: 'new',
|
|
@@ -494,14 +479,17 @@ describe('acl', () => {
|
|
|
494
479
|
const UserRepo = db.getCollection('users').repository;
|
|
495
480
|
const user = await UserRepo.create({
|
|
496
481
|
values: {
|
|
497
|
-
roles: ['new']
|
|
498
|
-
}
|
|
482
|
+
roles: ['new'],
|
|
483
|
+
},
|
|
499
484
|
});
|
|
500
485
|
|
|
501
486
|
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
|
|
502
|
-
const userAgent = app.agent().auth(
|
|
503
|
-
|
|
504
|
-
|
|
487
|
+
const userAgent = app.agent().auth(
|
|
488
|
+
userPlugin.jwtService.sign({
|
|
489
|
+
userId: user.get('id'),
|
|
490
|
+
}),
|
|
491
|
+
{ type: 'bearer' },
|
|
492
|
+
);
|
|
505
493
|
|
|
506
494
|
const schema = {
|
|
507
495
|
'x-uid': 'test',
|
package/src/__tests__/prepare.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import PluginUsers from '@nocobase/plugin-users';
|
|
2
|
+
import PluginErrorHandler from '@nocobase/plugin-error-handler';
|
|
2
3
|
import PluginCollectionManager from '@nocobase/plugin-collection-manager';
|
|
3
4
|
import PluginUiSchema from '@nocobase/plugin-ui-schema-storage';
|
|
4
5
|
import { mockServer } from '@nocobase/test';
|
|
5
6
|
import PluginACL from '../server';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
8
|
export async function prepareApp() {
|
|
10
9
|
const app = mockServer({
|
|
11
10
|
registerActions: true,
|
|
@@ -15,8 +14,8 @@ export async function prepareApp() {
|
|
|
15
14
|
|
|
16
15
|
app.plugin(PluginUsers);
|
|
17
16
|
app.plugin(PluginUiSchema);
|
|
17
|
+
app.plugin(PluginErrorHandler);
|
|
18
18
|
app.plugin(PluginCollectionManager);
|
|
19
|
-
|
|
20
19
|
app.plugin(PluginACL);
|
|
21
20
|
await app.loadAndInstall();
|
|
22
21
|
|
|
@@ -32,14 +32,17 @@ describe('role api', () => {
|
|
|
32
32
|
const UserRepo = db.getCollection('users').repository;
|
|
33
33
|
admin = await UserRepo.create({
|
|
34
34
|
values: {
|
|
35
|
-
roles: ['admin']
|
|
36
|
-
}
|
|
35
|
+
roles: ['admin'],
|
|
36
|
+
},
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
|
|
40
|
-
adminAgent = app.agent().auth(
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
adminAgent = app.agent().auth(
|
|
41
|
+
userPlugin.jwtService.sign({
|
|
42
|
+
userId: admin.get('id'),
|
|
43
|
+
}),
|
|
44
|
+
{ type: 'bearer' },
|
|
45
|
+
);
|
|
43
46
|
});
|
|
44
47
|
|
|
45
48
|
it('should list actions', async () => {
|
|
@@ -49,15 +52,14 @@ describe('role api', () => {
|
|
|
49
52
|
|
|
50
53
|
it('should grant universal role actions', async () => {
|
|
51
54
|
// grant role actions
|
|
52
|
-
const response = await adminAgent
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
actions: ['create:all', 'view:own'],
|
|
58
|
-
},
|
|
55
|
+
const response = await adminAgent.resource('roles').update({
|
|
56
|
+
forceUpdate: true,
|
|
57
|
+
values: {
|
|
58
|
+
strategy: {
|
|
59
|
+
actions: ['create:all', 'view:own'],
|
|
59
60
|
},
|
|
60
|
-
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
61
63
|
|
|
62
64
|
expect(response.statusCode).toEqual(200);
|
|
63
65
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Database from '@nocobase/database';
|
|
2
|
+
import { MockServer } from '@nocobase/test';
|
|
3
|
+
import { prepareApp } from './prepare';
|
|
4
|
+
|
|
5
|
+
describe('actions', () => {
|
|
6
|
+
let app: MockServer;
|
|
7
|
+
let db: Database;
|
|
8
|
+
let adminUser;
|
|
9
|
+
let agent;
|
|
10
|
+
let adminAgent;
|
|
11
|
+
let pluginUser;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
process.env.INIT_ROOT_EMAIL = 'test@nocobase.com';
|
|
15
|
+
process.env.INIT_ROOT_PASSWORD = '123456';
|
|
16
|
+
process.env.INIT_ROOT_NICKNAME = 'Test';
|
|
17
|
+
|
|
18
|
+
app = await prepareApp();
|
|
19
|
+
db = app.db;
|
|
20
|
+
|
|
21
|
+
pluginUser = app.getPlugin('@nocobase/plugin-users');
|
|
22
|
+
adminUser = await db.getRepository('users').findOne({
|
|
23
|
+
filter: {
|
|
24
|
+
email: process.env.INIT_ROOT_EMAIL
|
|
25
|
+
},
|
|
26
|
+
appends: ['roles']
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
agent = app.agent();
|
|
30
|
+
adminAgent = app.agent().auth(
|
|
31
|
+
pluginUser.jwtService.sign({
|
|
32
|
+
userId: adminUser.get('id'),
|
|
33
|
+
}),
|
|
34
|
+
{ type: 'bearer' },
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
await db.close();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('update profile with roles', async () => {
|
|
43
|
+
const res2 = await adminAgent.resource('users').updateProfile({
|
|
44
|
+
filterByTk: adminUser.id,
|
|
45
|
+
values: {
|
|
46
|
+
nickname: 'a',
|
|
47
|
+
roles: adminUser.roles
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
expect(res2.status).toBe(200);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/server.ts
CHANGED
|
@@ -418,21 +418,23 @@ export class PluginACL extends Plugin {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
const User = this.db.getCollection('users');
|
|
421
|
+
|
|
421
422
|
await User.repository.update({
|
|
422
423
|
values: {
|
|
423
|
-
roles: ['root', 'admin', 'member']
|
|
424
|
-
}
|
|
424
|
+
roles: ['root', 'admin', 'member'],
|
|
425
|
+
},
|
|
426
|
+
forceUpdate: true,
|
|
425
427
|
});
|
|
426
428
|
|
|
427
429
|
const RolesUsers = this.db.getCollection('rolesUsers');
|
|
428
430
|
await RolesUsers.repository.update({
|
|
429
431
|
filter: {
|
|
430
432
|
userId: 1,
|
|
431
|
-
roleName: 'root'
|
|
433
|
+
roleName: 'root',
|
|
432
434
|
},
|
|
433
435
|
values: {
|
|
434
|
-
default: true
|
|
435
|
-
}
|
|
436
|
+
default: true,
|
|
437
|
+
},
|
|
436
438
|
});
|
|
437
439
|
}
|
|
438
440
|
|