@windward/integrations 0.17.0 → 0.18.0
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/CHANGELOG.md +9 -0
- package/components/Integration/Driver/LoginSamlButton.vue +119 -0
- package/components/Integration/Driver/ManageSaml.vue +327 -0
- package/components/SecretField.vue +62 -5
- package/config/integration.config.js +2 -0
- package/helpers/Driver/SamlSso.ts +12 -0
- package/i18n/en-US/components/integration/driver.ts +23 -0
- package/i18n/en-US/pages/login/index.ts +2 -0
- package/i18n/en-US/pages/login/saml.ts +7 -0
- package/i18n/es-ES/components/integration/driver.ts +23 -0
- package/i18n/es-ES/pages/login/index.ts +2 -0
- package/i18n/es-ES/pages/login/saml.ts +7 -0
- package/i18n/sv-SE/components/integration/driver.ts +23 -0
- package/i18n/sv-SE/pages/login/index.ts +2 -0
- package/i18n/sv-SE/pages/login/saml.ts +7 -0
- package/jest.config.js +3 -0
- package/models/Auth/Saml.ts +21 -0
- package/package.json +1 -1
- package/plugin.js +17 -0
- package/test/__mocks__/componentsMock.js +81 -1
- package/test/setup.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Release [0.18.0] - 2025-10-10
|
|
4
|
+
|
|
5
|
+
* Merged in bugfix/LE-2135-saml-implementation (pull request #104)
|
|
6
|
+
* Merged in feature/LE-2135-saml-implementation (pull request #103)
|
|
7
|
+
* Merge branch 'release/0.18.0' into feature/LE-2135-saml-implementation
|
|
8
|
+
* Merged in feature/LE-2135-saml-implementation (pull request #99)
|
|
9
|
+
* Merge remote-tracking branch 'origin/release/0.18.0' into feature/LE-2135-saml-implementation
|
|
10
|
+
|
|
11
|
+
|
|
3
12
|
## Release [0.17.0] - 2025-09-22
|
|
4
13
|
|
|
5
14
|
* Merged in feature/LE-2035/7-strikes (pull request #98)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="samlEnabled">
|
|
3
|
+
<v-btn
|
|
4
|
+
color="primary"
|
|
5
|
+
elevation="0"
|
|
6
|
+
large
|
|
7
|
+
block
|
|
8
|
+
@click="loginWithSaml"
|
|
9
|
+
:loading="ssoLoading || loading"
|
|
10
|
+
:disabled="loading"
|
|
11
|
+
>
|
|
12
|
+
<v-icon class="pr-2">{{ samlButtonIcon }}</v-icon>
|
|
13
|
+
{{ samlButtonLabel }}
|
|
14
|
+
</v-btn>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
import Saml from '../../../models/Auth/Saml'
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
name: 'LoginSamlButton',
|
|
23
|
+
props: {
|
|
24
|
+
loading: {
|
|
25
|
+
type: Boolean,
|
|
26
|
+
default: false,
|
|
27
|
+
},
|
|
28
|
+
valid: {
|
|
29
|
+
type: Boolean,
|
|
30
|
+
default: true,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
data() {
|
|
34
|
+
return {
|
|
35
|
+
ssoLoading: false,
|
|
36
|
+
samlEnabled: false,
|
|
37
|
+
samlButtonLabel: 'Sign in with SSO',
|
|
38
|
+
samlButtonIcon: 'mdi-login',
|
|
39
|
+
organizationId: null,
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
mounted() {
|
|
43
|
+
this.checkSamlStatus()
|
|
44
|
+
},
|
|
45
|
+
methods: {
|
|
46
|
+
async checkSamlStatus() {
|
|
47
|
+
try {
|
|
48
|
+
// Get organization from store or context
|
|
49
|
+
const organization = this.$store.getters['organization/get']
|
|
50
|
+
|
|
51
|
+
// If no organization in store, try to use the hostname
|
|
52
|
+
let orgIdentifier = null
|
|
53
|
+
|
|
54
|
+
if (organization && organization.id) {
|
|
55
|
+
// Prefer using the organization ID if available
|
|
56
|
+
orgIdentifier = organization.id
|
|
57
|
+
} else {
|
|
58
|
+
// Fallback to using the hostname (full base_url)
|
|
59
|
+
orgIdentifier = window.location.hostname
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!orgIdentifier) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if SAML is enabled for this organization
|
|
67
|
+
const response = await Saml.custom('saml/status').get()
|
|
68
|
+
|
|
69
|
+
// The API returns an array, so we need to get the first element
|
|
70
|
+
const samlData = Array.isArray(response)
|
|
71
|
+
? response[0]
|
|
72
|
+
: response
|
|
73
|
+
|
|
74
|
+
if (samlData && samlData.enabled && samlData.configured) {
|
|
75
|
+
this.samlEnabled = true
|
|
76
|
+
this.organizationId = samlData.organization_id
|
|
77
|
+
|
|
78
|
+
// Use button settings from the integration configuration
|
|
79
|
+
this.samlButtonLabel =
|
|
80
|
+
samlData.button_label || 'Sign in with SSO'
|
|
81
|
+
this.samlButtonIcon = samlData.button_icon || 'mdi-login'
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// SAML not configured or error checking status
|
|
85
|
+
// Silently fail - SSO button won't show
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async loginWithSaml() {
|
|
89
|
+
this.ssoLoading = true
|
|
90
|
+
try {
|
|
91
|
+
// Get the SSO URL from the backend
|
|
92
|
+
const response = await Saml.custom('users/saml-login').get()
|
|
93
|
+
|
|
94
|
+
// The API returns an array, so we need to get the first element
|
|
95
|
+
const loginData = Array.isArray(response)
|
|
96
|
+
? response[0]
|
|
97
|
+
: response
|
|
98
|
+
|
|
99
|
+
if (loginData && loginData.sso_url) {
|
|
100
|
+
// Redirect to the IdP for authentication
|
|
101
|
+
window.location.href = loginData.sso_url
|
|
102
|
+
} else {
|
|
103
|
+
this.$dialog.error('Unable to initiate SSO login')
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Only log error in development mode
|
|
107
|
+
if (process.env.NODE_ENV === 'development') {
|
|
108
|
+
console.error('SSO login failed:', error)
|
|
109
|
+
}
|
|
110
|
+
this.$dialog.error(
|
|
111
|
+
'SSO login failed. Please try again or use standard login.'
|
|
112
|
+
)
|
|
113
|
+
} finally {
|
|
114
|
+
this.ssoLoading = false
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
</script>
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="!render" class="integration-loading">
|
|
4
|
+
<v-progress-circular size="128" indeterminate />
|
|
5
|
+
</div>
|
|
6
|
+
<div v-if="render">
|
|
7
|
+
<v-row justify="center" align="center" class="mt-5">
|
|
8
|
+
<v-col cols="12">
|
|
9
|
+
<v-switch
|
|
10
|
+
v-model="integration.enabled"
|
|
11
|
+
:label="
|
|
12
|
+
$t(
|
|
13
|
+
'windward.integrations.components.integration.driver.enabled'
|
|
14
|
+
)
|
|
15
|
+
"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<v-text-field
|
|
19
|
+
id="saml-entity-id"
|
|
20
|
+
v-model="integration.metadata.config.idp_entity_id"
|
|
21
|
+
:label="
|
|
22
|
+
$t(
|
|
23
|
+
'windward.integrations.components.integration.driver.saml_sso.idp_entity_id'
|
|
24
|
+
)
|
|
25
|
+
"
|
|
26
|
+
:hint="
|
|
27
|
+
$t(
|
|
28
|
+
'windward.integrations.components.integration.driver.saml_sso.idp_entity_id_hint'
|
|
29
|
+
)
|
|
30
|
+
"
|
|
31
|
+
:rules="$Validation.getRule('exists')"
|
|
32
|
+
></v-text-field>
|
|
33
|
+
|
|
34
|
+
<v-text-field
|
|
35
|
+
id="saml-sso-url"
|
|
36
|
+
v-model="integration.metadata.config.idp_sso_url"
|
|
37
|
+
:label="
|
|
38
|
+
$t(
|
|
39
|
+
'windward.integrations.components.integration.driver.saml_sso.idp_sso_url'
|
|
40
|
+
)
|
|
41
|
+
"
|
|
42
|
+
:hint="
|
|
43
|
+
$t(
|
|
44
|
+
'windward.integrations.components.integration.driver.saml_sso.idp_sso_url_hint'
|
|
45
|
+
)
|
|
46
|
+
"
|
|
47
|
+
:rules="$Validation.getRule('url')"
|
|
48
|
+
></v-text-field>
|
|
49
|
+
|
|
50
|
+
<v-text-field
|
|
51
|
+
id="saml-slo-url"
|
|
52
|
+
v-model="integration.metadata.config.idp_slo_url"
|
|
53
|
+
autocomplete="off"
|
|
54
|
+
:label="
|
|
55
|
+
$t(
|
|
56
|
+
'windward.integrations.components.integration.driver.saml_sso.idp_slo_url'
|
|
57
|
+
)
|
|
58
|
+
"
|
|
59
|
+
:hint="
|
|
60
|
+
$t(
|
|
61
|
+
'windward.integrations.components.integration.driver.saml_sso.idp_slo_url_hint'
|
|
62
|
+
)
|
|
63
|
+
"
|
|
64
|
+
:rules="$Validation.getRule('url')"
|
|
65
|
+
></v-text-field>
|
|
66
|
+
|
|
67
|
+
<SecretField
|
|
68
|
+
id="saml-certificate"
|
|
69
|
+
v-model="integration.metadata.config.idp_x509_cert"
|
|
70
|
+
tag="v-textarea"
|
|
71
|
+
autocomplete="off"
|
|
72
|
+
:label="
|
|
73
|
+
$t(
|
|
74
|
+
'windward.integrations.components.integration.driver.saml_sso.idp_x509_cert'
|
|
75
|
+
)
|
|
76
|
+
"
|
|
77
|
+
:hint="
|
|
78
|
+
$t(
|
|
79
|
+
'windward.integrations.components.integration.driver.saml_sso.idp_x509_cert_hint'
|
|
80
|
+
)
|
|
81
|
+
"
|
|
82
|
+
:append-icon="
|
|
83
|
+
showCertificate ? 'mdi-eye' : 'mdi-eye-off'
|
|
84
|
+
"
|
|
85
|
+
rows="4"
|
|
86
|
+
:rules="$Validation.getRule('exists')"
|
|
87
|
+
:readonly="!showCertificate"
|
|
88
|
+
:hidden="!showCertificate"
|
|
89
|
+
:type="showCertificate ? 'text' : 'password'"
|
|
90
|
+
@click:append="showCertificate = !showCertificate"
|
|
91
|
+
></SecretField>
|
|
92
|
+
|
|
93
|
+
<v-alert
|
|
94
|
+
v-if="integration.metadata.config.idp_x509_cert"
|
|
95
|
+
type="success"
|
|
96
|
+
text
|
|
97
|
+
dense
|
|
98
|
+
class="mt-2"
|
|
99
|
+
>
|
|
100
|
+
<v-icon small left>mdi-lock</v-icon>
|
|
101
|
+
{{
|
|
102
|
+
$t(
|
|
103
|
+
'windward.integrations.components.integration.driver.saml_sso.cert_stored'
|
|
104
|
+
)
|
|
105
|
+
}}
|
|
106
|
+
</v-alert>
|
|
107
|
+
|
|
108
|
+
<v-divider class="my-4" />
|
|
109
|
+
|
|
110
|
+
<h3 class="mb-3">
|
|
111
|
+
{{
|
|
112
|
+
$t(
|
|
113
|
+
'windward.integrations.components.integration.driver.saml_sso.button_settings'
|
|
114
|
+
)
|
|
115
|
+
}}
|
|
116
|
+
</h3>
|
|
117
|
+
|
|
118
|
+
<v-text-field
|
|
119
|
+
id="saml-button-label"
|
|
120
|
+
v-model="integration.metadata.config.button_label"
|
|
121
|
+
:label="
|
|
122
|
+
$t(
|
|
123
|
+
'windward.integrations.components.integration.driver.saml_sso.button_label'
|
|
124
|
+
)
|
|
125
|
+
"
|
|
126
|
+
:hint="
|
|
127
|
+
$t(
|
|
128
|
+
'windward.integrations.components.integration.driver.saml_sso.button_label_hint'
|
|
129
|
+
)
|
|
130
|
+
"
|
|
131
|
+
:placeholder="
|
|
132
|
+
$t(
|
|
133
|
+
'windward.integrations.components.integration.driver.saml_sso.button_label_default'
|
|
134
|
+
)
|
|
135
|
+
"
|
|
136
|
+
></v-text-field>
|
|
137
|
+
|
|
138
|
+
<v-text-field
|
|
139
|
+
id="saml-button-icon"
|
|
140
|
+
v-model="integration.metadata.config.button_icon"
|
|
141
|
+
:label="
|
|
142
|
+
$t(
|
|
143
|
+
'windward.integrations.components.integration.driver.saml_sso.button_icon'
|
|
144
|
+
)
|
|
145
|
+
"
|
|
146
|
+
:hint="
|
|
147
|
+
$t(
|
|
148
|
+
'windward.integrations.components.integration.driver.saml_sso.button_icon_hint'
|
|
149
|
+
)
|
|
150
|
+
"
|
|
151
|
+
placeholder="mdi-login"
|
|
152
|
+
>
|
|
153
|
+
<template #prepend>
|
|
154
|
+
<v-icon>{{
|
|
155
|
+
integration.metadata.config.button_icon ||
|
|
156
|
+
'mdi-login'
|
|
157
|
+
}}</v-icon>
|
|
158
|
+
</template>
|
|
159
|
+
</v-text-field>
|
|
160
|
+
|
|
161
|
+
<v-divider class="my-4" />
|
|
162
|
+
|
|
163
|
+
<v-alert type="info" outlined>
|
|
164
|
+
<div class="font-weight-bold mb-2">
|
|
165
|
+
{{
|
|
166
|
+
$t(
|
|
167
|
+
'windward.integrations.components.integration.driver.saml_sso.sp_details'
|
|
168
|
+
)
|
|
169
|
+
}}
|
|
170
|
+
</div>
|
|
171
|
+
<div class="text--secondary">
|
|
172
|
+
{{
|
|
173
|
+
$t(
|
|
174
|
+
'windward.integrations.components.integration.driver.saml_sso.sp_details_description'
|
|
175
|
+
)
|
|
176
|
+
}}
|
|
177
|
+
</div>
|
|
178
|
+
<div class="mt-3">
|
|
179
|
+
<div>
|
|
180
|
+
<strong
|
|
181
|
+
>{{
|
|
182
|
+
$t(
|
|
183
|
+
'windward.integrations.components.integration.driver.saml_sso.sp_entity_id_label'
|
|
184
|
+
)
|
|
185
|
+
}}:</strong
|
|
186
|
+
>
|
|
187
|
+
{{ spEntityId }}
|
|
188
|
+
</div>
|
|
189
|
+
<div>
|
|
190
|
+
<strong
|
|
191
|
+
>{{
|
|
192
|
+
$t(
|
|
193
|
+
'windward.integrations.components.integration.driver.saml_sso.acs_url_label'
|
|
194
|
+
)
|
|
195
|
+
}}:</strong
|
|
196
|
+
>
|
|
197
|
+
{{ acsUrl }}
|
|
198
|
+
</div>
|
|
199
|
+
<div>
|
|
200
|
+
<strong
|
|
201
|
+
>{{
|
|
202
|
+
$t(
|
|
203
|
+
'windward.integrations.components.integration.driver.saml_sso.slo_url_label'
|
|
204
|
+
)
|
|
205
|
+
}}:</strong
|
|
206
|
+
>
|
|
207
|
+
{{ sloUrl }}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</v-alert>
|
|
211
|
+
|
|
212
|
+
<TestConnection
|
|
213
|
+
:disabled="
|
|
214
|
+
!integration.metadata.config.idp_entity_id ||
|
|
215
|
+
!integration.metadata.config.idp_sso_url ||
|
|
216
|
+
!integration.metadata.config.idp_slo_url ||
|
|
217
|
+
!integration.metadata.config.idp_x509_cert
|
|
218
|
+
"
|
|
219
|
+
:loading="testConnectionLoading"
|
|
220
|
+
:errors="errorMessage"
|
|
221
|
+
@click="onTestConnection"
|
|
222
|
+
></TestConnection>
|
|
223
|
+
</v-col>
|
|
224
|
+
</v-row>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</template>
|
|
228
|
+
|
|
229
|
+
<script>
|
|
230
|
+
import _ from 'lodash'
|
|
231
|
+
import TestConnection from '../TestConnection.vue'
|
|
232
|
+
import ManageBaseVue from './ManageBase.vue'
|
|
233
|
+
import SecretField from '../../SecretField.vue'
|
|
234
|
+
import Uuid from '~/helpers/Uuid'
|
|
235
|
+
|
|
236
|
+
export default {
|
|
237
|
+
name: 'ManageSamlDriver',
|
|
238
|
+
components: { TestConnection, SecretField },
|
|
239
|
+
extends: ManageBaseVue,
|
|
240
|
+
data() {
|
|
241
|
+
return {
|
|
242
|
+
// formValid: true|false If this form is "complete" and passed validation on THIS component. Defined and watched in ManageBase.vue
|
|
243
|
+
// render: true|false If we should show the form aka when validation has passed. Defined and managed in ManageBase.vue
|
|
244
|
+
// integration: { metadata: {...} } The integration object to write to. Defined and loaded in ManageBase.vue
|
|
245
|
+
showCertificate: false,
|
|
246
|
+
errorMessage: '',
|
|
247
|
+
testConnectionLoading: false,
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
computed: {
|
|
251
|
+
spEntityId() {
|
|
252
|
+
return `${process.env.BASE_URL}/saml/metadata`
|
|
253
|
+
},
|
|
254
|
+
acsUrl() {
|
|
255
|
+
return `${process.env.BASE_URL}/saml/acs`
|
|
256
|
+
},
|
|
257
|
+
sloUrl() {
|
|
258
|
+
return `${process.env.BASE_URL}/saml/slo`
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
methods: {
|
|
262
|
+
/**
|
|
263
|
+
* Lifecycle event called from ManageBase.vue when async fetch() completes.
|
|
264
|
+
* Once called this.integration will be available containing the integration model (or a new one)
|
|
265
|
+
*/
|
|
266
|
+
onIntegrationLoaded() {
|
|
267
|
+
// Initialize SAML config if not exists
|
|
268
|
+
if (!this.integration.metadata.config) {
|
|
269
|
+
this.integration.metadata.config = {
|
|
270
|
+
idp_entity_id: '',
|
|
271
|
+
idp_sso_url: '',
|
|
272
|
+
idp_slo_url: '',
|
|
273
|
+
idp_x509_cert: '',
|
|
274
|
+
button_label: '',
|
|
275
|
+
button_icon: 'mdi-login',
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Set defaults for button settings if not present
|
|
280
|
+
if (!this.integration.metadata.config.button_label) {
|
|
281
|
+
this.integration.metadata.config.button_label = ''
|
|
282
|
+
}
|
|
283
|
+
if (!this.integration.metadata.config.button_icon) {
|
|
284
|
+
this.integration.metadata.config.button_icon = 'mdi-login'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Show the certificate by default if this is a new integration without a cert
|
|
288
|
+
this.showCertificate = _.isEmpty(
|
|
289
|
+
_.get(this.integration, 'metadata.config.idp_x509_cert', null)
|
|
290
|
+
)
|
|
291
|
+
},
|
|
292
|
+
async onTestConnection() {
|
|
293
|
+
this.testConnectionLoading = true
|
|
294
|
+
let response = { result: false }
|
|
295
|
+
try {
|
|
296
|
+
response = await this.testConnection(this.integration.metadata)
|
|
297
|
+
|
|
298
|
+
if (response.result) {
|
|
299
|
+
this.errorMessage = ''
|
|
300
|
+
this.$dialog.success(
|
|
301
|
+
this.$t(
|
|
302
|
+
'windward.integrations.shared.error.connect_success'
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
} else {
|
|
306
|
+
this.errorMessage = response.message
|
|
307
|
+
this.$dialog.error(
|
|
308
|
+
this.$t(
|
|
309
|
+
'windward.integrations.shared.error.connect_fail'
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error(e)
|
|
315
|
+
this.$dialog.error(
|
|
316
|
+
this.$t('windward.integrations.shared.error.unknown')
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// We will indirectly validate the form via connection tests
|
|
321
|
+
// That way we can 100% confirm that the integration is valid
|
|
322
|
+
this.formValid = response.result
|
|
323
|
+
this.testConnectionLoading = false
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
</script>
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
2
|
+
<component
|
|
3
|
+
:is="activeComponent"
|
|
4
|
+
:id="'secret-field-' + key"
|
|
5
|
+
:name="'secret-field-' + key"
|
|
4
6
|
:value="value"
|
|
5
|
-
readonly
|
|
7
|
+
:readonly="readonly"
|
|
8
|
+
:autocomplete="autocomplete"
|
|
6
9
|
:type="fieldType"
|
|
7
10
|
:placeholder="placeholder"
|
|
8
11
|
:label="label"
|
|
12
|
+
:hint="hint"
|
|
13
|
+
:append-icon="appendIcon"
|
|
14
|
+
:rules="rules"
|
|
15
|
+
:rows="rows"
|
|
16
|
+
@input="$emit('input', $event)"
|
|
9
17
|
>
|
|
10
18
|
<template #append>
|
|
11
19
|
<v-btn icon elevation="0" @click="toggleClear">
|
|
@@ -16,13 +24,18 @@
|
|
|
16
24
|
<span class="sr-only">{{ $t('shared.forms.copy') }}</span>
|
|
17
25
|
</v-btn>
|
|
18
26
|
</template>
|
|
19
|
-
</
|
|
27
|
+
</component>
|
|
20
28
|
</template>
|
|
21
29
|
|
|
22
30
|
<script>
|
|
31
|
+
import Crypto from '~/helpers/Crypto'
|
|
32
|
+
import { VTextarea, VTextField } from 'vuetify/lib'
|
|
33
|
+
|
|
23
34
|
export default {
|
|
24
35
|
name: 'SecretField',
|
|
36
|
+
components: { VTextarea, VTextField },
|
|
25
37
|
props: {
|
|
38
|
+
tag: { type: String, required: false, default: 'v-text-field' },
|
|
26
39
|
value: { type: String, required: false, default: '' },
|
|
27
40
|
hidden: { type: Boolean, required: false, default: true },
|
|
28
41
|
copy: { type: Boolean, required: false, default: true },
|
|
@@ -34,14 +47,57 @@ export default {
|
|
|
34
47
|
type: String,
|
|
35
48
|
required: false,
|
|
36
49
|
},
|
|
50
|
+
hint: {
|
|
51
|
+
type: String,
|
|
52
|
+
required: false,
|
|
53
|
+
},
|
|
54
|
+
autocomplete: {
|
|
55
|
+
type: String,
|
|
56
|
+
required: false,
|
|
57
|
+
},
|
|
58
|
+
appendIcon: {
|
|
59
|
+
type: String,
|
|
60
|
+
required: false,
|
|
61
|
+
},
|
|
62
|
+
rules: {
|
|
63
|
+
type: Array,
|
|
64
|
+
required: false,
|
|
65
|
+
default: () => [],
|
|
66
|
+
},
|
|
67
|
+
rows: {
|
|
68
|
+
type: [String, Number],
|
|
69
|
+
required: false,
|
|
70
|
+
default: 5,
|
|
71
|
+
},
|
|
72
|
+
readonly: {
|
|
73
|
+
type: Boolean,
|
|
74
|
+
required: false,
|
|
75
|
+
default: true,
|
|
76
|
+
},
|
|
37
77
|
},
|
|
38
78
|
data() {
|
|
39
79
|
return {
|
|
40
80
|
showClear: false,
|
|
41
81
|
}
|
|
42
82
|
},
|
|
43
|
-
|
|
44
83
|
computed: {
|
|
84
|
+
activeComponent() {
|
|
85
|
+
let tag = this.tag
|
|
86
|
+
|
|
87
|
+
// If the field type is password we need to hard-change to textfield since other components don't support password text
|
|
88
|
+
if (this.fieldType === 'password') {
|
|
89
|
+
tag = 'v-text-field'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (tag === 'v-textarea') {
|
|
93
|
+
return VTextarea
|
|
94
|
+
} else {
|
|
95
|
+
return VTextField
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
key() {
|
|
99
|
+
return Crypto.id()
|
|
100
|
+
},
|
|
45
101
|
fieldType() {
|
|
46
102
|
if (this.showClear) {
|
|
47
103
|
return 'text'
|
|
@@ -56,6 +112,7 @@ export default {
|
|
|
56
112
|
methods: {
|
|
57
113
|
toggleClear() {
|
|
58
114
|
this.showClear = !this.showClear
|
|
115
|
+
this.$emit('click:append', this.showClear)
|
|
59
116
|
},
|
|
60
117
|
copyText(text) {
|
|
61
118
|
navigator.clipboard.writeText(text || '')
|
|
@@ -5,6 +5,7 @@ import Desire2Learn from '../helpers/Driver/Desire2Learn'
|
|
|
5
5
|
import Moodle from '../helpers/Driver/Moodle'
|
|
6
6
|
import GoogleClassroom from '../helpers/Driver/GoogleClassroom'
|
|
7
7
|
import Resourcespace from '../helpers/Driver/Resourcespace'
|
|
8
|
+
import SamlSso from '../helpers/Driver/SamlSso'
|
|
8
9
|
|
|
9
10
|
export default {
|
|
10
11
|
/**
|
|
@@ -22,5 +23,6 @@ export default {
|
|
|
22
23
|
moodle: { driver: Moodle },
|
|
23
24
|
google_classroom: { driver: GoogleClassroom },
|
|
24
25
|
resourcespace: { driver: Resourcespace },
|
|
26
|
+
saml_sso: { driver: SamlSso },
|
|
25
27
|
},
|
|
26
28
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import Manage from '../../components/Integration/Driver/ManageSaml.vue'
|
|
3
|
+
import DriverInterface, { IntegrationComponents } from './DriverInterface'
|
|
4
|
+
import BaseDriver from './BaseDriver'
|
|
5
|
+
|
|
6
|
+
export default class SamlSso extends BaseDriver implements DriverInterface {
|
|
7
|
+
public components(): IntegrationComponents {
|
|
8
|
+
return {
|
|
9
|
+
Manage,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -34,6 +34,29 @@ export default {
|
|
|
34
34
|
username: 'Username',
|
|
35
35
|
key: 'API Key',
|
|
36
36
|
},
|
|
37
|
+
saml_sso: {
|
|
38
|
+
manage_dialog_title: 'Manage SAML SSO Integration',
|
|
39
|
+
idp_entity_id: 'Identity Provider Entity ID',
|
|
40
|
+
idp_entity_id_hint: 'The unique identifier for your IdP (e.g., http://www.okta.com/exk1fxphPNZPCOB3v0g3)',
|
|
41
|
+
idp_sso_url: 'Single Sign-On URL',
|
|
42
|
+
idp_sso_url_hint: 'The URL where users will be redirected for authentication',
|
|
43
|
+
idp_slo_url: 'Single Logout URL (Optional)',
|
|
44
|
+
idp_slo_url_hint: 'The URL for logging out from the IdP',
|
|
45
|
+
idp_x509_cert: 'X.509 Certificate',
|
|
46
|
+
idp_x509_cert_hint: 'Paste the certificate content from your IdP metadata (without BEGIN/END lines)',
|
|
47
|
+
sp_details: 'Service Provider Details',
|
|
48
|
+
sp_details_description: 'Provide these details to your Identity Provider administrator:',
|
|
49
|
+
cert_stored: 'Certificate securely stored',
|
|
50
|
+
button_settings: 'Login Button Settings',
|
|
51
|
+
button_label: 'Button Label',
|
|
52
|
+
button_label_hint: 'Text to display on the SSO login button',
|
|
53
|
+
button_label_default: 'Sign in with SSO',
|
|
54
|
+
button_icon: 'Button Icon',
|
|
55
|
+
button_icon_hint: 'Material Design Icon name (e.g., mdi-login, mdi-account-key)',
|
|
56
|
+
sp_entity_id_label: 'SP Entity ID',
|
|
57
|
+
acs_url_label: 'ACS URL',
|
|
58
|
+
slo_url_label: 'SLO URL',
|
|
59
|
+
},
|
|
37
60
|
enabled: 'Integration Enabled',
|
|
38
61
|
disabled: 'Integration Disabled',
|
|
39
62
|
ssl_enabled: 'SSL Enabled (Should be enabled for production)',
|
|
@@ -35,6 +35,29 @@ export default {
|
|
|
35
35
|
username: 'Nombre de usuario',
|
|
36
36
|
key: 'Clave API',
|
|
37
37
|
},
|
|
38
|
+
saml_sso: {
|
|
39
|
+
manage_dialog_title: 'Administrar integración SAML SSO',
|
|
40
|
+
idp_entity_id: 'ID de entidad del proveedor de identidad',
|
|
41
|
+
idp_entity_id_hint: 'El identificador único para su IdP (por ejemplo, http://www.okta.com/exk1fxphPNZPCOB3v0g3)',
|
|
42
|
+
idp_sso_url: 'URL de inicio de sesión único',
|
|
43
|
+
idp_sso_url_hint: 'La URL donde los usuarios serán redirigidos para autenticación',
|
|
44
|
+
idp_slo_url: 'URL de cierre de sesión único (Opcional)',
|
|
45
|
+
idp_slo_url_hint: 'La URL para cerrar sesión desde el IdP',
|
|
46
|
+
idp_x509_cert: 'Certificado X.509',
|
|
47
|
+
idp_x509_cert_hint: 'Pegue el contenido del certificado de los metadatos de su IdP (sin las líneas BEGIN/END)',
|
|
48
|
+
sp_details: 'Detalles del proveedor de servicios',
|
|
49
|
+
sp_details_description: 'Proporcione estos detalles al administrador de su proveedor de identidad:',
|
|
50
|
+
cert_stored: 'Certificado almacenado de forma segura',
|
|
51
|
+
button_settings: 'Configuración del botón de inicio de sesión',
|
|
52
|
+
button_label: 'Etiqueta del botón',
|
|
53
|
+
button_label_hint: 'Texto para mostrar en el botón de inicio de sesión SSO',
|
|
54
|
+
button_label_default: 'Iniciar sesión con SSO',
|
|
55
|
+
button_icon: 'Icono del botón',
|
|
56
|
+
button_icon_hint: 'Nombre del icono de Material Design (por ejemplo, mdi-login, mdi-account-key)',
|
|
57
|
+
sp_entity_id_label: 'ID de entidad SP',
|
|
58
|
+
acs_url_label: 'URL ACS',
|
|
59
|
+
slo_url_label: 'URL SLO',
|
|
60
|
+
},
|
|
38
61
|
enabled: 'Integración habilitada',
|
|
39
62
|
disabled: 'Integración deshabilitada',
|
|
40
63
|
ssl_enabled: 'SSL habilitado (debe estar habilitado para producción',
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
or: 'O',
|
|
3
|
+
processing: 'Procesando inicio de sesión SSO...',
|
|
4
|
+
success: '¡Inicio de sesión exitoso! Redirigiendo...',
|
|
5
|
+
failed: 'Error en el inicio de sesión SSO',
|
|
6
|
+
failed_description: 'Error en el inicio de sesión SSO. Por favor, inténtelo de nuevo.',
|
|
7
|
+
}
|
|
@@ -34,6 +34,29 @@ export default {
|
|
|
34
34
|
username: 'Användarnamn',
|
|
35
35
|
key: 'API Key',
|
|
36
36
|
},
|
|
37
|
+
saml_sso: {
|
|
38
|
+
manage_dialog_title: 'Hantera SAML SSO-integration',
|
|
39
|
+
idp_entity_id: 'Identitetsleverantörens enhets-ID',
|
|
40
|
+
idp_entity_id_hint: 'Den unika identifieraren för din IdP (t.ex. http://www.okta.com/exk1fxphPNZPCOB3v0g3)',
|
|
41
|
+
idp_sso_url: 'URL för enkel inloggning',
|
|
42
|
+
idp_sso_url_hint: 'URL:en dit användare omdirigeras för autentisering',
|
|
43
|
+
idp_slo_url: 'URL för enkel utloggning (Valfritt)',
|
|
44
|
+
idp_slo_url_hint: 'URL:en för att logga ut från IdP',
|
|
45
|
+
idp_x509_cert: 'X.509-certifikat',
|
|
46
|
+
idp_x509_cert_hint: 'Klistra in certifikatinnehållet från din IdP-metadata (utan BEGIN/END-rader)',
|
|
47
|
+
sp_details: 'Tjänsteleverantörsdetaljer',
|
|
48
|
+
sp_details_description: 'Ge dessa uppgifter till din identitetsleverantörsadministratör:',
|
|
49
|
+
cert_stored: 'Certifikat säkert lagrat',
|
|
50
|
+
button_settings: 'Inställningar för inloggningsknapp',
|
|
51
|
+
button_label: 'Knappetikett',
|
|
52
|
+
button_label_hint: 'Text att visa på SSO-inloggningsknappen',
|
|
53
|
+
button_label_default: 'Logga in med SSO',
|
|
54
|
+
button_icon: 'Knappikon',
|
|
55
|
+
button_icon_hint: 'Material Design-ikonnamn (t.ex. mdi-login, mdi-account-key)',
|
|
56
|
+
sp_entity_id_label: 'SP Entity ID',
|
|
57
|
+
acs_url_label: 'ACS URL',
|
|
58
|
+
slo_url_label: 'SLO URL',
|
|
59
|
+
},
|
|
37
60
|
enabled: 'Integration aktiverad',
|
|
38
61
|
disabled: 'Integration inaktiverad',
|
|
39
62
|
ssl_enabled: 'SSL aktiverat (Bör vara aktiverad för produktion)',
|
package/jest.config.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import Model from '~/models/Model'
|
|
3
|
+
|
|
4
|
+
export default class Saml extends Model {
|
|
5
|
+
get required(): string[] {
|
|
6
|
+
return []
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Set the resource route of the model
|
|
10
|
+
resource() {
|
|
11
|
+
return 'saml'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Static method for custom endpoints
|
|
15
|
+
static custom(endpoint: string): any {
|
|
16
|
+
const instance = new Saml()
|
|
17
|
+
// Override the resource for this specific instance
|
|
18
|
+
instance.resource = () => endpoint
|
|
19
|
+
return instance
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED
package/plugin.js
CHANGED
|
@@ -21,6 +21,8 @@ import ChatWindow from './components/Integration/AiAgentIntegration/ChatWindow.v
|
|
|
21
21
|
import AssessmentQuestionGenerateButton from './components/LLM/GenerateContent/AssessmentQuestionGenerateButton.vue'
|
|
22
22
|
import BlockQuestionGenerateButton from './components/LLM/GenerateContent/BlockQuestionGenerateButton.vue'
|
|
23
23
|
|
|
24
|
+
import LoginSamlButton from './components/Integration/Driver/LoginSamlButton.vue'
|
|
25
|
+
|
|
24
26
|
export default {
|
|
25
27
|
name: 'windward.integrations.name',
|
|
26
28
|
version: null,
|
|
@@ -66,6 +68,21 @@ export default {
|
|
|
66
68
|
},
|
|
67
69
|
],
|
|
68
70
|
components: {
|
|
71
|
+
userLogin: [
|
|
72
|
+
{
|
|
73
|
+
tag: 'windward-integrations-login-saml',
|
|
74
|
+
template: LoginSamlButton,
|
|
75
|
+
},
|
|
76
|
+
// Future SSO methods can be added here:
|
|
77
|
+
// {
|
|
78
|
+
// tag: 'windward-integrations-login-oauth2',
|
|
79
|
+
// template: LoginOAuth2Button,
|
|
80
|
+
// },
|
|
81
|
+
// {
|
|
82
|
+
// tag: 'windward-integrations-login-google',
|
|
83
|
+
// template: LoginGoogleButton,
|
|
84
|
+
// },
|
|
85
|
+
],
|
|
69
86
|
menu: [
|
|
70
87
|
{
|
|
71
88
|
ref_page: 'admin',
|
|
@@ -1 +1,81 @@
|
|
|
1
|
-
|
|
1
|
+
import Vue from 'vue'
|
|
2
|
+
|
|
3
|
+
Vue.component('SearchField', {
|
|
4
|
+
name: 'SearchField',
|
|
5
|
+
template: '<div>SearchField</div>',
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
Vue.component('v-expansion-panels', {
|
|
9
|
+
name: 'v-expansion-panels',
|
|
10
|
+
template: '<div>v-expansion-panels</div>',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
Vue.component('v-expansion-panel', {
|
|
14
|
+
name: 'v-expansion-panel',
|
|
15
|
+
template: '<div>v-expansion-panel</div>',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
Vue.component('v-expansion-panel-header', {
|
|
19
|
+
name: 'v-expansion-panel-header',
|
|
20
|
+
template: '<div>v-expansion-panel-header</div>',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
Vue.component('v-expansion-panel-content', {
|
|
24
|
+
name: 'v-expansion-panel-content',
|
|
25
|
+
template: '<div>v-expansion-panel-content</div>',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
Vue.component('v-tabs', {
|
|
29
|
+
name: 'v-tabs',
|
|
30
|
+
template: '<div>v-tabs</div>',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
Vue.component('v-tabs-slider', {
|
|
34
|
+
name: 'v-tabs-slider',
|
|
35
|
+
template: '<div>v-tabs-slider</div>',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
Vue.component('v-tab', {
|
|
39
|
+
name: 'v-tab',
|
|
40
|
+
template: '<div>v-tab</div>',
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
Vue.component('v-tabs-items', {
|
|
44
|
+
name: 'v-tabs-items',
|
|
45
|
+
template: '<div>v-tabs-items</div>',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
Vue.component('v-tab-item', {
|
|
49
|
+
name: 'v-tab-item',
|
|
50
|
+
template: '<div>v-tab-item</div>',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
Vue.component('v-select', {
|
|
54
|
+
name: 'v-select',
|
|
55
|
+
template: '<div>v-select</div>',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
Vue.component('v-autocomplete', {
|
|
59
|
+
name: 'v-autocomplete',
|
|
60
|
+
template: '<div>v-autocomplete</div>',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
Vue.component('v-switch', {
|
|
64
|
+
name: 'v-switch',
|
|
65
|
+
template: '<div>v-switch</div>',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
Vue.component('v-divider', {
|
|
69
|
+
name: 'v-divider',
|
|
70
|
+
template: '<div>v-divider</div>',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
Vue.component('v-progress-circular', {
|
|
74
|
+
name: 'v-progress-circular',
|
|
75
|
+
template: '<div>v-progress-circular</div>',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
Vue.component('v-treeview', {
|
|
79
|
+
name: 'v-treeview',
|
|
80
|
+
template: '<div>v-treeview</div>',
|
|
81
|
+
})
|
package/test/setup.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// This file is intentionally left blank.
|