@live-change/access-control-frontend 0.2.5 → 0.2.7
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/front/src/components/InsufficientAccess.vue +18 -0
- package/front/src/components/LimitedAccess.vue +19 -18
- package/front/src/configuration/AccessControl.vue +45 -22
- package/front/src/configuration/AccessInvitations.vue +8 -4
- package/front/src/configuration/AccessList.vue +10 -4
- package/front/src/configuration/AccessRequests.vue +13 -5
- package/front/src/configuration/PublicAccess.vue +10 -6
- package/index.js +2 -0
- package/package.json +17 -17
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex align-items-start p-4 bg-pink-100 border-round border-1 border-pink-300 mb-4">
|
|
3
|
+
<i class="pi pi-times-circle text-pink-900 text-2xl mr-3" />
|
|
4
|
+
<div class="mr-3">
|
|
5
|
+
<div class="text-pink-900 font-medium text-xl mb-3 line-height-1">Not authorized</div>
|
|
6
|
+
<p class="m-0 p-0 text-pink-700">
|
|
7
|
+
You do not have sufficient privileges to use this feature of this object.
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<style scoped>
|
|
17
|
+
|
|
18
|
+
</style>
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<slot v-if="!hidden && !authorized" name="blocked" :authorized="authorized" :roles="accessRoles" :accesses="accesses">
|
|
3
|
-
<
|
|
4
|
-
<i class="pi pi-times-circle text-pink-900 text-2xl mr-3" />
|
|
5
|
-
<div class="mr-3">
|
|
6
|
-
<div class="text-pink-900 font-medium text-xl mb-3 line-height-1">Not authorized</div>
|
|
7
|
-
<p class="m-0 p-0 text-pink-700">
|
|
8
|
-
You do not have sufficient privileges to use this feature of this object.
|
|
9
|
-
</p>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
3
|
+
<InsufficientAccess />
|
|
12
4
|
</slot>
|
|
13
5
|
<BlockUI v-if="!hidden" :blocked="!authorized">
|
|
14
6
|
<slot :authorized="authorized" :roles="accessRoles" :accesses="accesses"></slot>
|
|
@@ -21,6 +13,8 @@
|
|
|
21
13
|
|
|
22
14
|
import BlockUI from 'primevue/blockui'
|
|
23
15
|
|
|
16
|
+
import InsufficientAccess from "./InsufficientAccess.vue"
|
|
17
|
+
|
|
24
18
|
const props = defineProps({
|
|
25
19
|
objectType: {
|
|
26
20
|
type: String,
|
|
@@ -44,17 +38,24 @@
|
|
|
44
38
|
},
|
|
45
39
|
})
|
|
46
40
|
|
|
47
|
-
|
|
41
|
+
import { toRefs } from '@vueuse/core'
|
|
42
|
+
import { computed } from 'vue'
|
|
43
|
+
import { live, path } from '@live-change/vue3-ssr'
|
|
44
|
+
|
|
45
|
+
const { objectType, object, objects, requiredRoles } = toRefs(props)
|
|
48
46
|
|
|
49
|
-
const allObjects = ((
|
|
47
|
+
const allObjects = computed(() =>
|
|
48
|
+
((objectType.value && object.value) ? [{ objectType: objectType.value, object: object.value }] : [])
|
|
49
|
+
.concat(objects.value || [])
|
|
50
|
+
)
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const p = path()
|
|
53
|
+
const accessesLivePath = computed( () =>
|
|
54
|
+
p.accessControl.myAccessesTo({ objects: allObjects.value })
|
|
55
|
+
)
|
|
53
56
|
|
|
54
57
|
const [ accesses ] = await Promise.all([
|
|
55
|
-
live(
|
|
56
|
-
path().accessControl.myAccessesTo({ objects: allObjects })
|
|
57
|
-
)
|
|
58
|
+
live(accessesLivePath)
|
|
58
59
|
])
|
|
59
60
|
|
|
60
61
|
const accessRoles = computed(() => {
|
|
@@ -69,8 +70,8 @@
|
|
|
69
70
|
|
|
70
71
|
const authorized = computed(() => {
|
|
71
72
|
const clientRoles = accessRoles.value
|
|
72
|
-
if(requiredRoles.length == 0) return true
|
|
73
|
-
for(const requiredRolesOption of requiredRoles) {
|
|
73
|
+
if(requiredRoles.value.length == 0) return true
|
|
74
|
+
for(const requiredRolesOption of requiredRoles.value) {
|
|
74
75
|
if((Array.isArray(requiredRolesOption) ? requiredRolesOption : [requiredRolesOption])
|
|
75
76
|
.every(role => clientRoles.includes(role))
|
|
76
77
|
) return true
|
|
@@ -1,33 +1,39 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
+
<div v-if="myRoles.length > 0">
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
<div class="text-center">
|
|
6
|
+
<Button label="Invite with email" icon="pi pi-envelope" class="p-button mb-4"
|
|
7
|
+
@click="inviteDialog = true" :disabled="!canInvite" />
|
|
8
|
+
</div>
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
<InviteDialog v-if="isMounted"
|
|
11
|
+
:objectType="objectType" :object="object"
|
|
12
|
+
v-model:visible="inviteDialog"
|
|
13
|
+
:multiRole="multiRole"
|
|
14
|
+
:availableRoles="defaultInviteRoles" />
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
<AccessRequests :objectType="objectType" :object="object" :multiRole="multiRole"
|
|
17
|
+
:availableRoles="availableRoles" :disabled="!isAdmin" />
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<AccessInvitations :objectType="objectType" :object="object" :multiRole="multiRole"
|
|
20
|
+
:availableRoles="availableRoles" :disabled="!isAdmin" />
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
<AccessList :objectType="objectType" :object="object" :multiRole="multiRole"
|
|
23
|
+
:availableRoles="availableRoles"
|
|
24
|
+
:disabled="!isAdmin" />
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
<PublicAccess :objectType="objectType" :object="object" :multiRole="multiRole"
|
|
27
|
+
:availableSessionRoles="availablePublicSessionRoles ?? availablePublicRoles ?? availableRoles"
|
|
28
|
+
:availableUserRoles="availablePublicUserRoles ?? availablePublicRoles ?? availableRoles"
|
|
29
|
+
:availableRequestedRoles="availableRequestedRoles ?? availableRoles"
|
|
30
|
+
:sessionRolesVisible="sessionRolesVisible"
|
|
31
|
+
:userRolesVisible="userRolesVisible"
|
|
32
|
+
:requestedRolesVisible="requestedRolesVisible"
|
|
33
|
+
:disabled="!isAdmin"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
<InsufficientAccess v-else />
|
|
31
37
|
|
|
32
38
|
</div>
|
|
33
39
|
</template>
|
|
@@ -40,6 +46,7 @@
|
|
|
40
46
|
import AccessInvitations from "./AccessInvitations.vue"
|
|
41
47
|
import AccessList from "./AccessList.vue"
|
|
42
48
|
import PublicAccess from "./PublicAccess.vue"
|
|
49
|
+
import InsufficientAccess from "../components/InsufficientAccess.vue"
|
|
43
50
|
|
|
44
51
|
const {
|
|
45
52
|
object, objectType, paths,
|
|
@@ -109,4 +116,20 @@
|
|
|
109
116
|
|
|
110
117
|
const inviteDialog = ref(false)
|
|
111
118
|
|
|
119
|
+
import { path, live } from "@live-change/vue3-ssr"
|
|
120
|
+
|
|
121
|
+
const p = path()
|
|
122
|
+
const accessLivePath = computed( () =>
|
|
123
|
+
p.accessControl.myAccessTo({ objectType, object })
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const [ access ] = await Promise.all([
|
|
127
|
+
live(accessLivePath)
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
const myRoles = computed(() => access.value ? access.value.roles : [])
|
|
131
|
+
|
|
132
|
+
const isAdmin = computed(() => myRoles.value.includes('administrator'))
|
|
133
|
+
const canInvite = computed(() => myRoles.value.length > 0)
|
|
134
|
+
|
|
112
135
|
</script>
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
:optionLabel="optionLabel"
|
|
15
15
|
:modelValue="access.roles?.[0] ?? 'none'"
|
|
16
16
|
@update:modelValue="newValue => access.roles = [newValue]"
|
|
17
|
-
:feedback="false" toggleMask />
|
|
17
|
+
:feedback="false" toggleMask :disabled="disabled" />
|
|
18
18
|
<MultiSelect v-else id="userPublicAccess"
|
|
19
19
|
style="width: calc(100% - 2.357rem) !important"
|
|
20
20
|
:options="availableRoles"
|
|
21
21
|
:optionLabel="optionLabel"
|
|
22
22
|
v-model="access.roles"
|
|
23
|
-
:feedback="false" toggleMask />
|
|
23
|
+
:feedback="false" toggleMask :disabled="disabled" />
|
|
24
24
|
<Button @click="deleteAccessInvitation(access)" icon="pi pi-times"
|
|
25
25
|
class="p-button-rounded p-button-text p-button-plain ml-2 px-3"
|
|
26
|
-
style="padding-top: 0.77rem" />
|
|
26
|
+
style="padding-top: 0.77rem" :disabled="disabled" />
|
|
27
27
|
</div>
|
|
28
28
|
</div>
|
|
29
29
|
</div>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
import { computed, watch, ref, onMounted } from 'vue'
|
|
47
47
|
|
|
48
|
-
const { object, objectType, availableRoles, multiRole } = defineProps({
|
|
48
|
+
const { object, objectType, availableRoles, multiRole, disabled } = defineProps({
|
|
49
49
|
object: {
|
|
50
50
|
type: String,
|
|
51
51
|
required: true
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
multiRole: {
|
|
62
62
|
type: Boolean,
|
|
63
63
|
default: false
|
|
64
|
+
},
|
|
65
|
+
disabled: {
|
|
66
|
+
type: Boolean,
|
|
67
|
+
default: false
|
|
64
68
|
}
|
|
65
69
|
})
|
|
66
70
|
|
|
@@ -15,16 +15,18 @@
|
|
|
15
15
|
:optionLabel="optionLabel"
|
|
16
16
|
:modelValue="access.roles?.[0] ?? 'none'"
|
|
17
17
|
@update:modelValue="newValue => access.roles = [newValue]"
|
|
18
|
-
:feedback="false" toggleMask
|
|
18
|
+
:feedback="false" toggleMask
|
|
19
|
+
:disabled="disabled" />
|
|
19
20
|
<MultiSelect v-else id="userPublicAccess"
|
|
20
21
|
style="width: calc(100% - 2.357rem) !important"
|
|
21
22
|
:options="availableRoles"
|
|
22
23
|
:optionLabel="optionLabel"
|
|
23
24
|
v-model="access.roles"
|
|
24
|
-
:feedback="false" toggleMask
|
|
25
|
+
:feedback="false" toggleMask
|
|
26
|
+
:disabled="disabled" />
|
|
25
27
|
<Button @click="deleteAccess(access)" icon="pi pi-times"
|
|
26
28
|
class="p-button-rounded p-button-text p-button-plain ml-2 px-3"
|
|
27
|
-
style="padding-top: 0.77rem" />
|
|
29
|
+
style="padding-top: 0.77rem" :disabled="disabled" />
|
|
28
30
|
</div>
|
|
29
31
|
</div>
|
|
30
32
|
</div>
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
|
|
47
49
|
import { computed, watch, ref, onMounted } from 'vue'
|
|
48
50
|
|
|
49
|
-
const { object, objectType, availableRoles, multiRole } = defineProps({
|
|
51
|
+
const { object, objectType, availableRoles, multiRole, disabled } = defineProps({
|
|
50
52
|
object: {
|
|
51
53
|
type: String,
|
|
52
54
|
required: true
|
|
@@ -62,6 +64,10 @@
|
|
|
62
64
|
multiRole: {
|
|
63
65
|
type: Boolean,
|
|
64
66
|
default: false
|
|
67
|
+
},
|
|
68
|
+
disabled: {
|
|
69
|
+
type: Boolean,
|
|
70
|
+
default: false
|
|
65
71
|
}
|
|
66
72
|
})
|
|
67
73
|
|
|
@@ -14,19 +14,23 @@
|
|
|
14
14
|
:optionLabel="optionLabel"
|
|
15
15
|
:modelValue="access.roles?.[0] ?? 'none'"
|
|
16
16
|
@update:modelValue="newValue => access.roles = [newValue]"
|
|
17
|
-
:feedback="false" toggleMask
|
|
17
|
+
:feedback="false" toggleMask
|
|
18
|
+
:disabled="disabled" />
|
|
18
19
|
<MultiSelect v-else id="userPublicAccess"
|
|
19
20
|
style="width: calc(100% - 4.714rem) !important"
|
|
20
21
|
:options="availableRoles"
|
|
21
22
|
:optionLabel="optionLabel"
|
|
22
23
|
v-model="access.roles"
|
|
23
|
-
:feedback="false" toggleMask
|
|
24
|
+
:feedback="false" toggleMask
|
|
25
|
+
:disabled="disabled" />
|
|
24
26
|
<Button @click="acceptAccessRequest(access)" icon="pi pi-check"
|
|
25
27
|
class="p-button-rounded p-button-text p-button-plain ml-2 px-3"
|
|
26
|
-
style="padding-top: 0.77rem"
|
|
28
|
+
style="padding-top: 0.77rem"
|
|
29
|
+
:disabled="disabled" />
|
|
27
30
|
<Button @click="deleteAccessRequest(access)" icon="pi pi-times"
|
|
28
31
|
class="p-button-rounded p-button-text p-button-plain px-3"
|
|
29
|
-
style="padding-top: 0.77rem"
|
|
32
|
+
style="padding-top: 0.77rem"
|
|
33
|
+
:disabled="disabled" />
|
|
30
34
|
</div>
|
|
31
35
|
</div>
|
|
32
36
|
</div>
|
|
@@ -48,7 +52,7 @@
|
|
|
48
52
|
|
|
49
53
|
import { computed, watch, ref, onMounted } from 'vue'
|
|
50
54
|
|
|
51
|
-
const { object, objectType, availableRoles, multiRole } = defineProps({
|
|
55
|
+
const { object, objectType, availableRoles, multiRole, disabled } = defineProps({
|
|
52
56
|
object: {
|
|
53
57
|
type: String,
|
|
54
58
|
required: true
|
|
@@ -64,6 +68,10 @@
|
|
|
64
68
|
multiRole: {
|
|
65
69
|
type: Boolean,
|
|
66
70
|
default: false
|
|
71
|
+
},
|
|
72
|
+
disabled: {
|
|
73
|
+
type: Boolean,
|
|
74
|
+
default: false
|
|
67
75
|
}
|
|
68
76
|
})
|
|
69
77
|
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
:optionLabel="optionLabel"
|
|
9
9
|
:modelValue="synchronizedPublicAccess.sessionRoles?.[0] ?? 'none'"
|
|
10
10
|
@update:modelValue="newValue => synchronizedPublicAccess.sessionRoles = [newValue]"
|
|
11
|
-
:feedback="false" toggleMask />
|
|
11
|
+
:feedback="false" toggleMask :disabled="disabled" />
|
|
12
12
|
<MultiSelect v-else
|
|
13
13
|
id="publicAccess" class="w-full" inputClass="w-full"
|
|
14
14
|
:options="availableSessionRoles"
|
|
15
15
|
:optionLabel="optionLabel"
|
|
16
16
|
v-model="synchronizedPublicAccess.sessionRoles"
|
|
17
|
-
:feedback="false" toggleMask />
|
|
17
|
+
:feedback="false" toggleMask :disabled="disabled" />
|
|
18
18
|
</div>
|
|
19
19
|
<div class="p-field field mb-4 col-12 md:col-6" v-if="isMounted && userRolesVisible">
|
|
20
20
|
<label for="userPublicAccess" class="block text-900 font-medium mb-2">Public access for users:</label>
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
:optionLabel="optionLabel"
|
|
25
25
|
:modelValue="synchronizedPublicAccess.userRoles?.[0] ?? 'none'"
|
|
26
26
|
@update:modelValue="newValue => synchronizedPublicAccess.userRoles = [newValue]"
|
|
27
|
-
:feedback="false" toggleMask />
|
|
27
|
+
:feedback="false" toggleMask :disabled="disabled" />
|
|
28
28
|
<MultiSelect v-else id="userPublicAccess" class="w-full" inputClass="w-full"
|
|
29
29
|
:options="availableUserRoles"
|
|
30
30
|
:optionLabel="optionLabel"
|
|
31
31
|
v-model="synchronizedPublicAccess.userRoles"
|
|
32
|
-
:feedback="false" toggleMask />
|
|
32
|
+
:feedback="false" toggleMask :disabled="disabled" />
|
|
33
33
|
</div>
|
|
34
34
|
<div class="p-field field mb-4 col-12" v-if="isMounted && requestedRolesVisible">
|
|
35
35
|
<label for="availablePublicAccess" class="block text-900 font-medium mb-2">Roles available to request:</label>
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
:options="availableRequestedRoles"
|
|
38
38
|
:optionLabel="optionLabel"
|
|
39
39
|
v-model="synchronizedPublicAccess.availableRoles"
|
|
40
|
-
:feedback="false" toggleMask />
|
|
40
|
+
:feedback="false" toggleMask :disabled="disabled" />
|
|
41
41
|
</div>
|
|
42
42
|
</div>
|
|
43
43
|
</template>
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
object, objectType,
|
|
63
63
|
availableRequestedRoles, availableSessionRoles, availableUserRoles,
|
|
64
64
|
sessionRolesVisible, userRolesVisible, requestedRolesVisible,
|
|
65
|
-
multiRole
|
|
65
|
+
multiRole, disabled
|
|
66
66
|
} = defineProps({
|
|
67
67
|
object: {
|
|
68
68
|
type: String,
|
|
@@ -99,6 +99,10 @@
|
|
|
99
99
|
multiRole: {
|
|
100
100
|
type: Boolean,
|
|
101
101
|
default: false
|
|
102
|
+
},
|
|
103
|
+
disabled: {
|
|
104
|
+
type: Boolean,
|
|
105
|
+
default: false
|
|
102
106
|
}
|
|
103
107
|
})
|
|
104
108
|
|
package/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import LimitedAccess from "./front/src/components/LimitedAccess.vue"
|
|
2
2
|
export { LimitedAccess }
|
|
3
|
+
import InsufficientAccess from "./front/src/components/InsufficientAccess.vue"
|
|
4
|
+
export { InsufficientAccess }
|
|
3
5
|
|
|
4
6
|
import AccessControl from "./front/src/configuration/AccessControl.vue"
|
|
5
7
|
import AccessInvitations from "./front/src/configuration/AccessInvitations.vue"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/access-control-frontend",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"memDev": "lcli memDev --enableSessions --initScript ./init.js --dbAccess",
|
|
6
6
|
"localDevInit": "rm tmp.db; lcli localDev --enableSessions --initScript ./init.js",
|
|
@@ -20,20 +20,20 @@
|
|
|
20
20
|
"debug": "node --inspect-brk server"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@live-change/access-control-service": "0.3.
|
|
24
|
-
"@live-change/cli": "0.7.
|
|
25
|
-
"@live-change/dao": "0.5.
|
|
26
|
-
"@live-change/dao-vue3": "0.5.
|
|
27
|
-
"@live-change/dao-websocket": "0.5.
|
|
28
|
-
"@live-change/db-admin": "0.5.
|
|
29
|
-
"@live-change/framework": "0.7.
|
|
30
|
-
"@live-change/frontend-base": "^0.2.
|
|
31
|
-
"@live-change/password-authentication-service": "0.3.
|
|
32
|
-
"@live-change/secret-code-service": "0.3.
|
|
33
|
-
"@live-change/secret-link-service": "0.3.
|
|
34
|
-
"@live-change/session-service": "0.3.
|
|
35
|
-
"@live-change/user-frontend": "^0.2.
|
|
36
|
-
"@live-change/user-service": "0.3.
|
|
23
|
+
"@live-change/access-control-service": "0.3.2",
|
|
24
|
+
"@live-change/cli": "0.7.4",
|
|
25
|
+
"@live-change/dao": "0.5.8",
|
|
26
|
+
"@live-change/dao-vue3": "0.5.8",
|
|
27
|
+
"@live-change/dao-websocket": "0.5.8",
|
|
28
|
+
"@live-change/db-admin": "0.5.19",
|
|
29
|
+
"@live-change/framework": "0.7.4",
|
|
30
|
+
"@live-change/frontend-base": "^0.2.7",
|
|
31
|
+
"@live-change/password-authentication-service": "0.3.2",
|
|
32
|
+
"@live-change/secret-code-service": "0.3.2",
|
|
33
|
+
"@live-change/secret-link-service": "0.3.2",
|
|
34
|
+
"@live-change/session-service": "0.3.2",
|
|
35
|
+
"@live-change/user-frontend": "^0.2.7",
|
|
36
|
+
"@live-change/user-service": "0.3.2",
|
|
37
37
|
"@live-change/vue3-components": "0.2.15",
|
|
38
38
|
"@live-change/vue3-ssr": "0.2.15",
|
|
39
39
|
"@vueuse/core": "^9.1.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"vue3-scroll-border": "0.1.2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@live-change/codeceptjs-helper": "0.7.
|
|
56
|
+
"@live-change/codeceptjs-helper": "0.7.4",
|
|
57
57
|
"@wdio/selenium-standalone-service": "^7.20.8",
|
|
58
58
|
"codeceptjs": "^3.3.4",
|
|
59
59
|
"generate-password": "1.7.0",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"author": "",
|
|
66
66
|
"license": "BSD-3-Clause",
|
|
67
67
|
"description": "",
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "ef577fb31dd857acb491bd7114e611638283fe6f"
|
|
69
69
|
}
|