@live-change/access-control-frontend 0.0.3
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/e2e/codecept.conf.js +60 -0
- package/e2e/steps.d.ts +12 -0
- package/e2e/steps_file.js +11 -0
- package/front/index.html +11 -0
- package/front/public/favicon.ico +0 -0
- package/front/public/images/empty-photo.svg +38 -0
- package/front/public/images/empty-user-photo.svg +33 -0
- package/front/public/images/logo.svg +34 -0
- package/front/public/images/logo128.png +0 -0
- package/front/src/App.vue +34 -0
- package/front/src/NavBar.vue +103 -0
- package/front/src/components/LimitedAccess.vue +10 -0
- package/front/src/configuration/AccessControl.vue +117 -0
- package/front/src/configuration/AccessInvitations.vue +118 -0
- package/front/src/configuration/AccessList.vue +119 -0
- package/front/src/configuration/AccessRequests.vue +132 -0
- package/front/src/configuration/PublicAccess.vue +123 -0
- package/front/src/configuration/routes.js +10 -0
- package/front/src/entry-client.js +6 -0
- package/front/src/entry-server.js +6 -0
- package/front/src/invite/InviteDialog.vue +117 -0
- package/front/src/invite/InviteEmail.vue +112 -0
- package/front/src/invite/routes.js +11 -0
- package/front/src/notifications/InviteNotification.vue +71 -0
- package/front/src/notifications/index.js +6 -0
- package/front/src/router.js +55 -0
- package/front/vite.config.js +11 -0
- package/package.json +75 -0
- package/server/init.js +84 -0
- package/server/security.config.js +53 -0
- package/server/services.config.js +84 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="synchronizedAccesses.length > 0" class="mb-4">
|
|
3
|
+
<div class="text-900 font-medium text-xl mb-2">Authorized</div>
|
|
4
|
+
<div v-for="access of synchronizedAccesses" :key="access.to"
|
|
5
|
+
class="flex flex-row flex-wrap align-items-center">
|
|
6
|
+
<div class="col-12 md:col-6 py-1">
|
|
7
|
+
<UserIdentification :ownerType="access.sessionOrUserType" :owner="access.sessionOrUser"
|
|
8
|
+
:data="access.identification" />
|
|
9
|
+
</div>
|
|
10
|
+
<div class="col-12 md:col-6 flex flex-row pr-0" v-if="isMounted">
|
|
11
|
+
<Dropdown v-if="!multiRole && (access.roles?.length ?? 0) <= 1"
|
|
12
|
+
id="userPublicAccess" class="w-14em"
|
|
13
|
+
style="width: calc(100% - 2.357rem) !important"
|
|
14
|
+
:options="['none'].concat(availableRoles)"
|
|
15
|
+
:optionLabel="optionLabel"
|
|
16
|
+
:modelValue="access.roles?.[0] ?? 'none'"
|
|
17
|
+
@update:modelValue="newValue => access.roles = [newValue]"
|
|
18
|
+
:feedback="false" toggleMask />
|
|
19
|
+
<MultiSelect v-else id="userPublicAccess"
|
|
20
|
+
style="width: calc(100% - 2.357rem) !important"
|
|
21
|
+
:options="availableRoles"
|
|
22
|
+
:optionLabel="optionLabel"
|
|
23
|
+
v-model="access.roles"
|
|
24
|
+
:feedback="false" toggleMask />
|
|
25
|
+
<Button @click="deleteAccess(access)" icon="pi pi-times"
|
|
26
|
+
class="p-button-rounded p-button-text p-button-plain ml-2 px-3"
|
|
27
|
+
style="padding-top: 0.77rem" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup>
|
|
34
|
+
|
|
35
|
+
import Button from "primevue/button"
|
|
36
|
+
import Dropdown from "primevue/dropdown"
|
|
37
|
+
import MultiSelect from "primevue/multiselect"
|
|
38
|
+
|
|
39
|
+
import { useToast } from 'primevue/usetoast'
|
|
40
|
+
const toast = useToast()
|
|
41
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
42
|
+
const confirm = useConfirm()
|
|
43
|
+
|
|
44
|
+
import { UserIdentification } from "@live-change/user-frontend"
|
|
45
|
+
import { synchronized, synchronizedList } from "@live-change/vue3-components"
|
|
46
|
+
|
|
47
|
+
import { computed, watch, ref, onMounted } from 'vue'
|
|
48
|
+
|
|
49
|
+
const { object, objectType, availableRoles, multiRole } = defineProps({
|
|
50
|
+
object: {
|
|
51
|
+
type: String,
|
|
52
|
+
required: true
|
|
53
|
+
},
|
|
54
|
+
objectType: {
|
|
55
|
+
type: String,
|
|
56
|
+
required: true
|
|
57
|
+
},
|
|
58
|
+
availableRoles: {
|
|
59
|
+
type: Array,
|
|
60
|
+
default: () => ['reader']
|
|
61
|
+
},
|
|
62
|
+
multiRole: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: false
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const isMounted = ref(false)
|
|
69
|
+
onMounted(() => isMounted.value = true)
|
|
70
|
+
|
|
71
|
+
function optionLabel(option) {
|
|
72
|
+
if(option == 'none') return 'none'
|
|
73
|
+
return option
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
import { path, live, actions } from '@live-change/vue3-ssr'
|
|
77
|
+
const accessControlApi = actions().accessControl
|
|
78
|
+
|
|
79
|
+
const [ accesses ] = await Promise.all([
|
|
80
|
+
live(path().accessControl.objectOwnedAccesses({ object, objectType })
|
|
81
|
+
.with(access => path().userIdentification.sessionOrUserOwnedIdentification({
|
|
82
|
+
sessionOrUserType: access.sessionOrUserType, sessionOrUser: access.sessionOrUser
|
|
83
|
+
}).bind('identification'))
|
|
84
|
+
)
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
const synchronizedAccessesList = synchronizedList({
|
|
88
|
+
source: accesses,
|
|
89
|
+
update: accessControlApi.updateSessionOrUserAndObjectOwnedAccess,
|
|
90
|
+
delete: accessControlApi.resetSessionOrUserAndObjectOwnedAccess,
|
|
91
|
+
identifiers: { object, objectType },
|
|
92
|
+
objectIdentifiers: ({ to, sessionOrUser, sessionOrUserType }) =>
|
|
93
|
+
({ access: to, sessionOrUser, sessionOrUserType, object, objectType }),
|
|
94
|
+
onSave: () => toast.add({ severity: 'info', summary: 'Access saved', life: 1500 }),
|
|
95
|
+
recursive: true
|
|
96
|
+
})
|
|
97
|
+
const synchronizedAccesses = synchronizedAccessesList.value
|
|
98
|
+
|
|
99
|
+
function deleteAccess(access) {
|
|
100
|
+
console.log("DELETE ACCESS", access)
|
|
101
|
+
confirm.require({
|
|
102
|
+
target: event.currentTarget,
|
|
103
|
+
message: `Do you want to revoke user "${access.identification.name}" access?`,
|
|
104
|
+
icon: 'pi pi-info-circle',
|
|
105
|
+
acceptClass: 'p-button-danger',
|
|
106
|
+
accept: async () => {
|
|
107
|
+
await synchronizedAccessesList.delete(access)
|
|
108
|
+
/*await accessControlApi.deleteObjectRelatedAccess({
|
|
109
|
+
access: access.to, object: access.object, objectType: access.objectType
|
|
110
|
+
})*/
|
|
111
|
+
toast.add({ severity:'info', summary: 'Access Revoked', life: 1500 })
|
|
112
|
+
},
|
|
113
|
+
reject: () => {
|
|
114
|
+
toast.add({ severity:'error', summary: 'Rejected', detail: 'You have rejected', life: 3000 })
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
</script>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="synchronizedAccessRequests.length > 0" class="mb-4">
|
|
3
|
+
<div class="text-900 font-medium text-xl mb-2">Access Requests</div>
|
|
4
|
+
<div v-for="access of synchronizedAccessRequests" :key="access.to"
|
|
5
|
+
class="flex flex-row flex-wrap align-items-center">
|
|
6
|
+
<div class="col-12 md:col-6 py-1">
|
|
7
|
+
<UserIdentification :ownerType="access.sessionOrUserType" :owner="access.sessionOrUser"
|
|
8
|
+
:data="access.identification" />
|
|
9
|
+
</div>
|
|
10
|
+
<div class="col-12 md:col-6 flex flex-row pr-0" v-if="isMounted">
|
|
11
|
+
<Dropdown v-if="!multiRole && (access.roles?.length ?? 0) <= 1" id="userPublicAccess" class="w-14em"
|
|
12
|
+
style="width: calc(100% - 4.714rem) !important"
|
|
13
|
+
:options="['none'].concat(availableRoles)"
|
|
14
|
+
:optionLabel="optionLabel"
|
|
15
|
+
:modelValue="access.roles?.[0] ?? 'none'"
|
|
16
|
+
@update:modelValue="newValue => access.roles = [newValue]"
|
|
17
|
+
:feedback="false" toggleMask />
|
|
18
|
+
<MultiSelect v-else id="userPublicAccess"
|
|
19
|
+
style="width: calc(100% - 4.714rem) !important"
|
|
20
|
+
:options="availableRoles"
|
|
21
|
+
:optionLabel="optionLabel"
|
|
22
|
+
v-model="access.roles"
|
|
23
|
+
:feedback="false" toggleMask />
|
|
24
|
+
<Button @click="acceptAccessRequest(access)" icon="pi pi-check"
|
|
25
|
+
class="p-button-rounded p-button-text p-button-plain ml-2 px-3"
|
|
26
|
+
style="padding-top: 0.77rem" />
|
|
27
|
+
<Button @click="deleteAccessRequest(access)" icon="pi pi-times"
|
|
28
|
+
class="p-button-rounded p-button-text p-button-plain px-3"
|
|
29
|
+
style="padding-top: 0.77rem" />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
|
|
37
|
+
import Button from "primevue/button"
|
|
38
|
+
import Dropdown from "primevue/dropdown"
|
|
39
|
+
import MultiSelect from "primevue/multiselect"
|
|
40
|
+
|
|
41
|
+
import { useToast } from 'primevue/usetoast'
|
|
42
|
+
const toast = useToast()
|
|
43
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
44
|
+
const confirm = useConfirm()
|
|
45
|
+
|
|
46
|
+
import { UserIdentification } from "@live-change/user-frontend"
|
|
47
|
+
import { synchronized, synchronizedList } from "@live-change/vue3-components"
|
|
48
|
+
|
|
49
|
+
import { computed, watch, ref, onMounted } from 'vue'
|
|
50
|
+
|
|
51
|
+
const { object, objectType, availableRoles, multiRole } = defineProps({
|
|
52
|
+
object: {
|
|
53
|
+
type: String,
|
|
54
|
+
required: true
|
|
55
|
+
},
|
|
56
|
+
objectType: {
|
|
57
|
+
type: String,
|
|
58
|
+
required: true
|
|
59
|
+
},
|
|
60
|
+
availableRoles: {
|
|
61
|
+
type: Array,
|
|
62
|
+
default: () => ['reader']
|
|
63
|
+
},
|
|
64
|
+
multiRole: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const isMounted = ref(false)
|
|
71
|
+
onMounted(() => isMounted.value = true)
|
|
72
|
+
|
|
73
|
+
function optionLabel(option) {
|
|
74
|
+
if(option == 'none') return 'none'
|
|
75
|
+
return option
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
import { path, live, actions } from '@live-change/vue3-ssr'
|
|
79
|
+
const accessControlApi = actions().accessControl
|
|
80
|
+
|
|
81
|
+
const [ accessRequests ] = await Promise.all([
|
|
82
|
+
live(path().accessControl.objectOwnedAccessRequests({ object, objectType })
|
|
83
|
+
.with(access => path().userIdentification.sessionOrUserOwnedIdentification({
|
|
84
|
+
sessionOrUserType: access.sessionOrUserType, sessionOrUser: access.sessionOrUser
|
|
85
|
+
}).bind('identification'))
|
|
86
|
+
.action('delete', ({ sessionOrUserType, sessionOrUser, objectType, object }) =>
|
|
87
|
+
path().accessControl.resetSessionOrUserAndObjectOwnedAccessRequest(
|
|
88
|
+
{ sessionOrUserType, sessionOrUser, objectType, object }
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
const synchronizedAccessRequestsList = synchronizedList({
|
|
95
|
+
source: accessRequests,
|
|
96
|
+
update: accessControlApi.updateSessionOrUserAndObjectOwnedAccessRequest,
|
|
97
|
+
delete: accessControlApi.resetSessionOrUserAndObjectOwnedAccessRequest,
|
|
98
|
+
identifiers: { object, objectType },
|
|
99
|
+
objectIdentifiers: ({ to, sessionOrUser, sessionOrUserType }) =>
|
|
100
|
+
({ accessRequest: to, sessionOrUser, sessionOrUserType, object, objectType }),
|
|
101
|
+
onSave: () => toast.add({ severity: 'info', summary: 'Access request saved', life: 1500 }),
|
|
102
|
+
recursive: true
|
|
103
|
+
})
|
|
104
|
+
const synchronizedAccessRequests = synchronizedAccessRequestsList.value
|
|
105
|
+
|
|
106
|
+
function deleteAccessRequest(accessRequest) {
|
|
107
|
+
console.log("DELETE ACCESS REQUEST", accessRequest)
|
|
108
|
+
confirm.require({
|
|
109
|
+
target: event.currentTarget,
|
|
110
|
+
message: `Do you want to delete user "${accessRequest.identification.name}" access request?`,
|
|
111
|
+
icon: 'pi pi-info-circle',
|
|
112
|
+
acceptClass: 'p-button-danger',
|
|
113
|
+
accept: async () => {
|
|
114
|
+
await synchronizedAccessRequestsList.delete(accessRequest)
|
|
115
|
+
//accessRequest.delete()
|
|
116
|
+
toast.add({ severity:'info', summary: 'Access Request Deleted', life: 1500 })
|
|
117
|
+
},
|
|
118
|
+
reject: () => {
|
|
119
|
+
toast.add({ severity:'error', summary: 'Rejected', detail: 'You have rejected', life: 3000 })
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function acceptAccessRequest(accessRequest) {
|
|
125
|
+
console.log("ACCEPT ACCESS REQUEST", accessRequest)
|
|
126
|
+
await accessControlApi.acceptAccessRequest({
|
|
127
|
+
...accessRequest, access: accessRequest.to
|
|
128
|
+
})
|
|
129
|
+
toast.add({ severity:'info', summary: 'Access Request accepted', life: 1500 })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
</script>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="grid formgrid p-fluid mb-2">
|
|
3
|
+
<div class="p-field field mb-4 col-12 md:col-6" v-if="isMounted && sessionRolesVisible">
|
|
4
|
+
<label for="publicAccess" class="block text-900 font-medium mb-2">Public access:</label>
|
|
5
|
+
<Dropdown v-if="!multiRole && (synchronizedPublicAccess.sessionRoles?.length ?? 0) <= 1"
|
|
6
|
+
id="publicAccess" class="w-full" inputClass="w-full"
|
|
7
|
+
:options="['none'].concat(availableSessionRoles)"
|
|
8
|
+
:optionLabel="optionLabel"
|
|
9
|
+
:modelValue="synchronizedPublicAccess.sessionRoles?.[0] ?? 'none'"
|
|
10
|
+
@update:modelValue="newValue => synchronizedPublicAccess.sessionRoles = [newValue]"
|
|
11
|
+
:feedback="false" toggleMask />
|
|
12
|
+
<MultiSelect v-else
|
|
13
|
+
id="publicAccess" class="w-full" inputClass="w-full"
|
|
14
|
+
:options="availableSessionRoles"
|
|
15
|
+
:optionLabel="optionLabel"
|
|
16
|
+
v-model="synchronizedPublicAccess.sessionRoles"
|
|
17
|
+
:feedback="false" toggleMask />
|
|
18
|
+
</div>
|
|
19
|
+
<div class="p-field field mb-4 col-12 md:col-6" v-if="isMounted && userRolesVisible">
|
|
20
|
+
<label for="userPublicAccess" class="block text-900 font-medium mb-2">Public access for users:</label>
|
|
21
|
+
<Dropdown v-if="!multiRole && (synchronizedPublicAccess.userRoles?.length ?? 0) <= 1"
|
|
22
|
+
id="userPublicAccess" class="w-full" inputClass="w-full"
|
|
23
|
+
:options="['none'].concat(availableUserRoles)"
|
|
24
|
+
:optionLabel="optionLabel"
|
|
25
|
+
:modelValue="synchronizedPublicAccess.userRoles?.[0] ?? 'none'"
|
|
26
|
+
@update:modelValue="newValue => synchronizedPublicAccess.userRoles = [newValue]"
|
|
27
|
+
:feedback="false" toggleMask />
|
|
28
|
+
<MultiSelect v-else id="userPublicAccess" class="w-full" inputClass="w-full"
|
|
29
|
+
:options="availableUserRoles"
|
|
30
|
+
:optionLabel="optionLabel"
|
|
31
|
+
v-model="synchronizedPublicAccess.userRoles"
|
|
32
|
+
:feedback="false" toggleMask />
|
|
33
|
+
</div>
|
|
34
|
+
<div class="p-field field mb-4 col-12" v-if="isMounted && requestedRolesVisible">
|
|
35
|
+
<label for="availablePublicAccess" class="block text-900 font-medium mb-2">Roles available to request:</label>
|
|
36
|
+
<MultiSelect id="userPublicAccess" class="w-full" inputClass="w-full"
|
|
37
|
+
:options="availableRequestedRoles"
|
|
38
|
+
:optionLabel="optionLabel"
|
|
39
|
+
v-model="synchronizedPublicAccess.availableRoles"
|
|
40
|
+
:feedback="false" toggleMask />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup>
|
|
46
|
+
import Dropdown from "primevue/dropdown"
|
|
47
|
+
import MultiSelect from "primevue/multiselect"
|
|
48
|
+
|
|
49
|
+
import { useToast } from 'primevue/usetoast'
|
|
50
|
+
const toast = useToast()
|
|
51
|
+
|
|
52
|
+
import { synchronized } from "@live-change/vue3-components"
|
|
53
|
+
|
|
54
|
+
function optionLabel(option) {
|
|
55
|
+
if(option == 'none') return 'none'
|
|
56
|
+
return option
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
import { computed, watch, ref, onMounted } from 'vue'
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
object, objectType,
|
|
63
|
+
availableRequestedRoles, availableSessionRoles, availableUserRoles,
|
|
64
|
+
sessionRolesVisible, userRolesVisible, requestedRolesVisible,
|
|
65
|
+
multiRole
|
|
66
|
+
} = defineProps({
|
|
67
|
+
object: {
|
|
68
|
+
type: String,
|
|
69
|
+
required: true
|
|
70
|
+
},
|
|
71
|
+
objectType: {
|
|
72
|
+
type: String,
|
|
73
|
+
required: true
|
|
74
|
+
},
|
|
75
|
+
sessionRolesVisible: {
|
|
76
|
+
type: Boolean,
|
|
77
|
+
default: true
|
|
78
|
+
},
|
|
79
|
+
userRolesVisible: {
|
|
80
|
+
type: Boolean,
|
|
81
|
+
default: true
|
|
82
|
+
},
|
|
83
|
+
requestedRolesVisible: {
|
|
84
|
+
type: Boolean,
|
|
85
|
+
default: true
|
|
86
|
+
},
|
|
87
|
+
availableRequestedRoles: {
|
|
88
|
+
type: Array,
|
|
89
|
+
default: () => ['reader']
|
|
90
|
+
},
|
|
91
|
+
availableSessionRoles: {
|
|
92
|
+
type: Array,
|
|
93
|
+
default: () => ['reader']
|
|
94
|
+
},
|
|
95
|
+
availableUserRoles: {
|
|
96
|
+
type: Array,
|
|
97
|
+
default: () => ['reader']
|
|
98
|
+
},
|
|
99
|
+
multiRole: {
|
|
100
|
+
type: Boolean,
|
|
101
|
+
default: false
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const isMounted = ref(false)
|
|
106
|
+
onMounted(() => isMounted.value = true)
|
|
107
|
+
|
|
108
|
+
import { path, live, actions } from '@live-change/vue3-ssr'
|
|
109
|
+
const accessControlApi = actions().accessControl
|
|
110
|
+
|
|
111
|
+
const [ publicAccess ] = await Promise.all([
|
|
112
|
+
live(path().accessControl.objectOwnedPublicAccess({ object, objectType }))
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
const synchronizedPublicAccess = synchronized({
|
|
116
|
+
source: publicAccess,
|
|
117
|
+
update: accessControlApi.setOrUpdateObjectOwnedPublicAccess,
|
|
118
|
+
identifiers: { object, objectType },
|
|
119
|
+
recursive: true,
|
|
120
|
+
onSave: () => toast.add({ severity: 'info', summary: 'Public access saved', life: 1500 })
|
|
121
|
+
}).value
|
|
122
|
+
|
|
123
|
+
</script>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Dialog :visible="visible" @update:visible="v => $emit('update:visible', v)"
|
|
3
|
+
:modal="true" class="w-full sm:w-9 md:w-8 lg:w-6">
|
|
4
|
+
<template #header>
|
|
5
|
+
<h3>Invite user with email</h3>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<command-form service="accessControl" action="inviteEmail"
|
|
9
|
+
ref="inviteForm"
|
|
10
|
+
v-slot="{ data }"
|
|
11
|
+
:parameters="{ objectType, object }"
|
|
12
|
+
:initialValues="{ roles: availableRoles }"
|
|
13
|
+
@done="handleInvited" keepOnDone>
|
|
14
|
+
|
|
15
|
+
<div class="flex flex-row flex-wrap align-items-center" style="margin-left: -0.5rem; margin-right: -0.5rem;">
|
|
16
|
+
<div class="col-12 md:col-6 py-1">
|
|
17
|
+
<div class="p-field mb-3">
|
|
18
|
+
<label for="email" class="block text-900 font-medium mb-2">
|
|
19
|
+
Email address
|
|
20
|
+
</label>
|
|
21
|
+
<InputText id="email" type="text" class="w-full"
|
|
22
|
+
aria-describedby="email-help" :class="{ 'p-invalid': data.emailError }"
|
|
23
|
+
v-model="data.email" />
|
|
24
|
+
<small id="email-help" class="p-error">{{ data.emailError }}</small>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="col-12 md:col-6">
|
|
28
|
+
<div class="p-field mb-3">
|
|
29
|
+
<label for="inviteAccess" class="block text-900 font-medium mb-2">
|
|
30
|
+
Roles
|
|
31
|
+
</label>
|
|
32
|
+
<Dropdown v-if="!multiRole" id="inviteAccess" class="w-14em w-full"
|
|
33
|
+
:options="['none'].concat(availableRoles)"
|
|
34
|
+
:optionLabel="optionLabel"
|
|
35
|
+
:modelValue="data.roles?.[0] ?? 'none'"
|
|
36
|
+
@update:modelValue="newValue => data.roles = [newValue]"
|
|
37
|
+
:feedback="false" toggleMask />
|
|
38
|
+
<MultiSelect v-if="multiRole" id="inviteAccess" class="w-full"
|
|
39
|
+
:options="availableRoles"
|
|
40
|
+
:optionLabel="optionLabel"
|
|
41
|
+
v-model="data.roles"
|
|
42
|
+
:feedback="false" toggleMask />
|
|
43
|
+
<small id="email-help" class="p-error">{{ data.rolesError }}</small>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="p-field mb-1">
|
|
49
|
+
<label for="inviteMessage" class="block text-900 font-medium mb-2">
|
|
50
|
+
Message ( optional )
|
|
51
|
+
</label>
|
|
52
|
+
<Textarea id="inviteMessage" v-model="data.message" :autoResize="true" rows="3" class="w-full" />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
</command-form>
|
|
56
|
+
|
|
57
|
+
<template #footer>
|
|
58
|
+
<Button label="Invite" icon="pi pi-envelope" autofocus @click="inviteForm.submit()" />
|
|
59
|
+
</template>
|
|
60
|
+
</Dialog>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<script setup>
|
|
64
|
+
import Button from "primevue/button"
|
|
65
|
+
import Dropdown from "primevue/dropdown"
|
|
66
|
+
import MultiSelect from "primevue/multiselect"
|
|
67
|
+
|
|
68
|
+
import Dialog from 'primevue/dialog'
|
|
69
|
+
import InputText from 'primevue/inputtext'
|
|
70
|
+
import Textarea from 'primevue/textarea'
|
|
71
|
+
|
|
72
|
+
import { useToast } from 'primevue/usetoast'
|
|
73
|
+
const toast = useToast()
|
|
74
|
+
|
|
75
|
+
import { ref } from 'vue'
|
|
76
|
+
import { toRefs } from "@vueuse/core"
|
|
77
|
+
|
|
78
|
+
const props = defineProps({
|
|
79
|
+
object: {
|
|
80
|
+
type: String,
|
|
81
|
+
required: true
|
|
82
|
+
},
|
|
83
|
+
objectType: {
|
|
84
|
+
type: String,
|
|
85
|
+
required: true
|
|
86
|
+
},
|
|
87
|
+
visible: {
|
|
88
|
+
type: Boolean,
|
|
89
|
+
required: true
|
|
90
|
+
},
|
|
91
|
+
availableRoles: {
|
|
92
|
+
type: Array,
|
|
93
|
+
default: () => ['reader']
|
|
94
|
+
},
|
|
95
|
+
multiRole: {
|
|
96
|
+
type: Boolean,
|
|
97
|
+
default: false
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const emit = defineEmits(['update:visible'])
|
|
102
|
+
|
|
103
|
+
const { visible, availableRoles, multiRole, object, objectType } = toRefs(props)
|
|
104
|
+
|
|
105
|
+
function optionLabel(option) {
|
|
106
|
+
if(option == 'none') return 'none'
|
|
107
|
+
return option
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const inviteForm = ref()
|
|
111
|
+
function handleInvited() {
|
|
112
|
+
emit('update:visible', false)
|
|
113
|
+
toast.add({ severity:'info', summary: 'Invitation sent!', life: 1500 })
|
|
114
|
+
console.log("INVITED", arguments)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
</script>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<pre data-headers>{{ JSON.stringify(metadata, null, ' ') }}</pre>
|
|
3
|
+
<div data-html class="message m-6">
|
|
4
|
+
<p class="text-lg">
|
|
5
|
+
Hello!
|
|
6
|
+
</p>
|
|
7
|
+
<p>
|
|
8
|
+
<span v-if="from?.name">
|
|
9
|
+
Our user
|
|
10
|
+
<strong>{{ from.name }}</strong>
|
|
11
|
+
</span>
|
|
12
|
+
<span v-else>One of our users</span>
|
|
13
|
+
invited you to use X by entering your email address.
|
|
14
|
+
</p>
|
|
15
|
+
<div v-if="data.message.trim().length > 0">
|
|
16
|
+
<p>He left message for you:</p>
|
|
17
|
+
<blockquote class="font-italic">{{ data.message }}</blockquote>
|
|
18
|
+
</div>
|
|
19
|
+
<p>
|
|
20
|
+
if you already have an account, you can add this email to your account
|
|
21
|
+
and the invitation will be linked to your account.
|
|
22
|
+
</p>
|
|
23
|
+
<p>
|
|
24
|
+
Click the button below:
|
|
25
|
+
</p>
|
|
26
|
+
<div>
|
|
27
|
+
<a :href="linkAddress" class="no-underline">
|
|
28
|
+
<Button label="Confirm email" class="p-button-lg" />
|
|
29
|
+
</a>
|
|
30
|
+
</div>
|
|
31
|
+
<p>
|
|
32
|
+
Or copy this address to your browser address bar:<br>
|
|
33
|
+
<a :href="linkAddress">
|
|
34
|
+
{{ linkAddress }}
|
|
35
|
+
</a>
|
|
36
|
+
</p>
|
|
37
|
+
<p>
|
|
38
|
+
Let us know in case it's not for you.
|
|
39
|
+
</p>
|
|
40
|
+
<p>
|
|
41
|
+
See you soon<br>
|
|
42
|
+
Live Change Team
|
|
43
|
+
</p>
|
|
44
|
+
<img src="/images/logo128.png">
|
|
45
|
+
</div>
|
|
46
|
+
<pre class="message" data-text>
|
|
47
|
+
Hello!
|
|
48
|
+
|
|
49
|
+
{{ from?.name ? `Our user ${from?.name}` : 'One of our users' }} invited you to use X by entering your email address.
|
|
50
|
+
|
|
51
|
+
Click link below or copy address to your browser address bar:
|
|
52
|
+
|
|
53
|
+
{{ linkAddress }}
|
|
54
|
+
|
|
55
|
+
Let us know in case it's not for you.
|
|
56
|
+
|
|
57
|
+
See you soon
|
|
58
|
+
Live Change Team
|
|
59
|
+
</pre>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup>
|
|
63
|
+
import Button from "primevue/button"
|
|
64
|
+
|
|
65
|
+
import { path, live, actions } from '@live-change/vue3-ssr'
|
|
66
|
+
|
|
67
|
+
const { action, contact, json } = defineProps({
|
|
68
|
+
contact: {
|
|
69
|
+
type: String,
|
|
70
|
+
required: true
|
|
71
|
+
},
|
|
72
|
+
json: {
|
|
73
|
+
type: String,
|
|
74
|
+
required: true
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const data = JSON.parse(json)
|
|
79
|
+
|
|
80
|
+
const secrets = data.secrets
|
|
81
|
+
const secretLink = secrets.find(secret => secret.type == 'link')
|
|
82
|
+
|
|
83
|
+
const [ from ] = await Promise.all([
|
|
84
|
+
live(path().userIdentification.sessionOrUserOwnedIdentification(
|
|
85
|
+
{ sessionOrUserType: data.fromType, sessionOrUser: data.from }))
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
const metadata = {
|
|
89
|
+
from: '<noreply@flipchart.live>',
|
|
90
|
+
subject: `${from?.name ?? 'Our user'} invited you to ${data.objectType}`,
|
|
91
|
+
to: contact
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const linkAddress = ENV_BASE_HREF + '/link/' + secretLink.secret.secretCode
|
|
95
|
+
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<style scoped>
|
|
99
|
+
img {
|
|
100
|
+
width: 100%;
|
|
101
|
+
max-width: 100px;
|
|
102
|
+
}
|
|
103
|
+
.message {
|
|
104
|
+
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
|
|
105
|
+
color: #495057;
|
|
106
|
+
font-weight: 400;
|
|
107
|
+
}
|
|
108
|
+
pre {
|
|
109
|
+
border-top: 1px solid black;
|
|
110
|
+
border-bottom: 1px solid black;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
export function routes(config = {}) {
|
|
3
|
+
const { prefix = '/', route = (r) => r } = config
|
|
4
|
+
|
|
5
|
+
return [
|
|
6
|
+
route({ name: 'accessControl:email:invite', path: '/_email/inviteWithMessage/:contact/:json',
|
|
7
|
+
props: true, meta: { raw: true }, component: () => import("./InviteEmail.vue") }),
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default routes
|