@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 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
- <v-text-field
3
- id="secret-field"
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
- </v-text-field>
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)',
@@ -1,5 +1,7 @@
1
1
  import lti from './lti'
2
+ import saml from './saml'
2
3
 
3
4
  export default {
4
5
  lti,
6
+ saml,
5
7
  }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ or: 'OR',
3
+ processing: 'Processing SSO login...',
4
+ success: 'Login successful! Redirecting...',
5
+ failed: 'SSO login failed',
6
+ failed_description: 'SSO login failed. Please try again.',
7
+ }
@@ -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',
@@ -1,5 +1,7 @@
1
1
  import lti from './lti'
2
+ import saml from './saml'
2
3
 
3
4
  export default {
4
5
  lti,
6
+ saml,
5
7
  }
@@ -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)',
@@ -1,5 +1,7 @@
1
1
  import lti from './lti'
2
+ import saml from './saml'
2
3
 
3
4
  export default {
4
5
  lti,
6
+ saml,
5
7
  }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ or: 'ELLER',
3
+ processing: 'Bearbetar SSO-inloggning...',
4
+ success: 'Inloggningen lyckades! Omdirigerar...',
5
+ failed: 'SSO-inloggning misslyckades',
6
+ failed_description: 'SSO-inloggning misslyckades. Vänligen försök igen.',
7
+ }
package/jest.config.js CHANGED
@@ -1,4 +1,7 @@
1
1
  module.exports = {
2
+ testEnvironment: 'jsdom',
3
+ setupFiles: ['<rootDir>/test/setup.js'],
4
+ testURL: 'http://localhost/',
2
5
  moduleNameMapper: {
3
6
  '^@/(.*)$': '<rootDir>/$1',
4
7
  '^vue$': 'vue/dist/vue.common.js',
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/integrations",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Windward UI Plugin Integrations for 3rd Party Systems",
5
5
  "main": "plugin.js",
6
6
  "scripts": {
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
- // Mock components from other repos
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.