@windward/integrations 0.17.0 → 0.19.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/components/Content/Blocks/ExternalIntegration/LtiConsumer.vue +3 -3
  3. package/components/Content/Blocks/ExternalIntegration/ScormConsumer.vue +34 -0
  4. package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue +10 -8
  5. package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumers.vue +2 -2
  6. package/components/ExternalIntegration/Driver/Lti1p1/ManageProvider.vue +8 -5
  7. package/components/ExternalIntegration/Driver/Lti1p1/ManageProviders.vue +3 -2
  8. package/components/ExternalIntegration/Driver/Lti1p3/ManageConsumer.vue +8 -6
  9. package/components/ExternalIntegration/Driver/Lti1p3/ManageConsumers.vue +2 -2
  10. package/components/ExternalIntegration/Driver/Lti1p3/ManageProvider.vue +27 -5
  11. package/components/ExternalIntegration/Driver/Lti1p3/ManageProviders.vue +4 -3
  12. package/components/ExternalIntegration/Driver/Lti1p3/ViewConsumer.vue +6 -5
  13. package/components/ExternalIntegration/Driver/ManageScorm.vue +45 -0
  14. package/components/ExternalIntegration/Driver/Scorm/ManageConsumer.vue +76 -0
  15. package/components/ExternalIntegration/Driver/Scorm/ManageConsumers.vue +233 -0
  16. package/components/ExternalIntegration/Driver/Scorm/ManageProvider.vue +475 -0
  17. package/components/ExternalIntegration/Driver/Scorm/ManageProviders.vue +299 -0
  18. package/components/Integration/Driver/LoginSamlButton.vue +119 -0
  19. package/components/Integration/Driver/ManageSaml.vue +327 -0
  20. package/components/LLM/GenerateContent/BlockQuestionGenerateButton.vue +34 -3
  21. package/components/SecretField.vue +99 -19
  22. package/components/Settings/ExternalIntegration/LtiConsumerSettings.vue +2 -2
  23. package/components/Settings/ExternalIntegration/ManageCourseIntegrationSettings.vue +6 -6
  24. package/components/Settings/ExternalIntegration/ScormConsumerSettings.vue +42 -0
  25. package/config/integration.config.js +2 -0
  26. package/helpers/Driver/SamlSso.ts +12 -0
  27. package/helpers/ExternalIntegration/ScormHelper.ts +155 -0
  28. package/i18n/en-US/components/external_integration/driver/lti1p3.ts +4 -1
  29. package/i18n/en-US/components/external_integration/driver/scorm.ts +14 -0
  30. package/i18n/en-US/components/external_integration/index.ts +3 -1
  31. package/i18n/en-US/components/integration/driver.ts +23 -0
  32. package/i18n/en-US/components/llm/generate_content/generate_questions.ts +7 -0
  33. package/i18n/en-US/pages/course/external_integration/index.ts +1 -1
  34. package/i18n/en-US/pages/login/index.ts +4 -0
  35. package/i18n/en-US/pages/login/lti.ts +2 -0
  36. package/i18n/en-US/pages/login/saml.ts +7 -0
  37. package/i18n/en-US/pages/login/scorm.ts +28 -0
  38. package/i18n/en-US/shared/content_blocks.ts +1 -0
  39. package/i18n/en-US/shared/settings.ts +1 -0
  40. package/i18n/es-ES/components/external_integration/driver/lti1p3.ts +4 -1
  41. package/i18n/es-ES/components/external_integration/driver/scorm.ts +15 -0
  42. package/i18n/es-ES/components/external_integration/index.ts +3 -1
  43. package/i18n/es-ES/components/integration/driver.ts +23 -0
  44. package/i18n/es-ES/components/llm/generate_content/generate_questions.ts +7 -0
  45. package/i18n/es-ES/pages/course/external_integration/index.ts +1 -1
  46. package/i18n/es-ES/pages/login/index.ts +4 -0
  47. package/i18n/es-ES/pages/login/lti.ts +2 -0
  48. package/i18n/es-ES/pages/login/saml.ts +7 -0
  49. package/i18n/es-ES/pages/login/scorm.ts +29 -0
  50. package/i18n/es-ES/shared/content_blocks.ts +1 -0
  51. package/i18n/es-ES/shared/settings.ts +1 -0
  52. package/i18n/sv-SE/components/external_integration/driver/lti1p3.ts +4 -1
  53. package/i18n/sv-SE/components/external_integration/driver/scorm.ts +14 -0
  54. package/i18n/sv-SE/components/external_integration/index.ts +3 -1
  55. package/i18n/sv-SE/components/integration/driver.ts +23 -0
  56. package/i18n/sv-SE/components/llm/generate_content/generate_questions.ts +7 -0
  57. package/i18n/sv-SE/pages/course/external_integration/index.ts +1 -1
  58. package/i18n/sv-SE/pages/login/index.ts +4 -0
  59. package/i18n/sv-SE/pages/login/lti.ts +2 -0
  60. package/i18n/sv-SE/pages/login/saml.ts +7 -0
  61. package/i18n/sv-SE/pages/login/scorm.ts +29 -0
  62. package/i18n/sv-SE/shared/content_blocks.ts +2 -1
  63. package/i18n/sv-SE/shared/settings.ts +1 -0
  64. package/jest.config.js +3 -0
  65. package/models/Auth/Saml.ts +21 -0
  66. package/models/ExternalIntegration/{LtiConsumer.ts → Consumer.ts} +2 -2
  67. package/models/ExternalIntegration/{LtiProvider.ts → Provider.ts} +2 -2
  68. package/package.json +2 -1
  69. package/pages/course/externalIntegration/index.vue +4 -0
  70. package/pages/login/scorm/error.vue +102 -0
  71. package/pages/login/scorm/promptEmail.vue +180 -0
  72. package/plugin.js +128 -7
  73. package/test/Components/ExternalIntegration/ManageScorm.spec.js +19 -0
  74. package/test/Components/ExternalIntegration/Scorm/ManageConsumer.spec.js +19 -0
  75. package/test/Components/ExternalIntegration/Scorm/ManageConsumers.spec.js +19 -0
  76. package/test/Components/ExternalIntegration/Scorm/ManageProvider.spec.js +19 -0
  77. package/test/Components/ExternalIntegration/Scorm/ManageProviders.spec.js +19 -0
  78. package/test/__mocks__/componentsMock.js +81 -1
  79. package/test/mocks.js +12 -0
  80. package/test/setup.js +1 -0
@@ -0,0 +1,299 @@
1
+ <template>
2
+ <div>
3
+ <DialogBox
4
+ color="primary"
5
+ action-save
6
+ action-save-new
7
+ @click:save="onSaved"
8
+ @click:save-new="onSaved"
9
+ >
10
+ <template #title>{{
11
+ $t(
12
+ 'windward.integrations.components.external_integration.driver.scorm.new'
13
+ )
14
+ }}</template>
15
+ <template #trigger>{{ $t('shared.forms.new') }}</template>
16
+ <template #form="{ on, attrs }"
17
+ ><ManageProvider v-bind="attrs" v-on="on"></ManageProvider
18
+ ></template>
19
+ </DialogBox>
20
+
21
+ <v-data-table
22
+ :headers="headers"
23
+ :items="providers"
24
+ :items-per-page="10"
25
+ class="elevation-1"
26
+ >
27
+ <template #[`item.target`]="{ item }">
28
+ <ProviderTargetViewer
29
+ class="field--target text-truncate"
30
+ :target="item.target"
31
+ ></ProviderTargetViewer>
32
+ </template>
33
+ <template #[`item.download_package`]="{ item }">
34
+ <v-btn
35
+ color="primary"
36
+ :loading="isDownloading"
37
+ @click="onClickDownload(item)"
38
+ >{{
39
+ $t(
40
+ 'windward.integrations.components.external_integration.driver.scorm.download_package'
41
+ )
42
+ }}</v-btn
43
+ >
44
+ </template>
45
+
46
+ <template #[`item.enabled`]="{ item }">
47
+ <v-icon :color="item.enabled ? 'success' : 'error'"
48
+ >{{ item.enabled ? 'mdi-check' : 'mdi-close' }}
49
+ </v-icon>
50
+ <span v-if="!item.enabled" class="sr-only">{{
51
+ $t('shared.forms.enabled')
52
+ }}</span>
53
+ </template>
54
+
55
+ <template #[`item.created_at`]="{ item }">
56
+ {{ $d(new Date(item.created_at), 'short') }}
57
+ </template>
58
+ <template #[`item.actions`]="{ index, item }">
59
+ <SpeedDial
60
+ direction="left"
61
+ color="primary"
62
+ transition="slide-x-reverse-transition"
63
+ >
64
+ <v-btn
65
+ color="error"
66
+ outlined
67
+ elevation="0"
68
+ class="outlined"
69
+ @click="onConfirmDelete(item)"
70
+ >
71
+ {{ $t('shared.forms.delete') }}
72
+ <span class="sr-only">{{
73
+ $t('shared.forms.delete')
74
+ }}</span>
75
+ </v-btn>
76
+ <DialogBox
77
+ color="primary"
78
+ outlined
79
+ :class-prop="'outlined'"
80
+ action-save
81
+ @click:save="onSaved"
82
+ >
83
+ <template #title>{{
84
+ $t(
85
+ 'windward.integrations.components.external_integration.driver.scorm.edit'
86
+ )
87
+ }}</template>
88
+ <template #trigger>
89
+ {{
90
+ $t(
91
+ 'windward.integrations.components.external_integration.driver.scorm.edit_display'
92
+ )
93
+ }}
94
+ <span class="sr-only">{{
95
+ $t(
96
+ 'windward.integrations.components.external_integration.driver.scorm.edit'
97
+ )
98
+ }}</span>
99
+ </template>
100
+ <template #form="{ on, attrs }"
101
+ ><ManageProvider
102
+ v-model="providers[index]"
103
+ v-bind="attrs"
104
+ v-on="on"
105
+ ></ManageProvider
106
+ ></template>
107
+ </DialogBox>
108
+ </SpeedDial>
109
+ </template>
110
+ </v-data-table>
111
+ </div>
112
+ </template>
113
+
114
+ <script>
115
+ import _ from 'lodash'
116
+ import { mapGetters } from 'vuex'
117
+ import Provider from '../../../../models/ExternalIntegration/Provider'
118
+ import SecretField from '../../../SecretField.vue'
119
+ import ProviderTargetViewer from '../../ProviderTargetViewer.vue'
120
+ import ManageProvider from './ManageProvider.vue'
121
+ import DialogBox from '~/components/Core/DialogBox.vue'
122
+ import Organization from '~/models/Organization'
123
+ import Course from '~/models/Course'
124
+ import SpeedDial from '~/components/Core/SpeedDial.vue'
125
+
126
+ export default {
127
+ name: 'ManageScormProvidersDriver',
128
+ components: {
129
+ SecretField,
130
+ DialogBox,
131
+ ManageProvider,
132
+ ProviderTargetViewer,
133
+ SpeedDial,
134
+ },
135
+ data() {
136
+ return {
137
+ providers: [],
138
+ headers: [
139
+ {
140
+ text: this.$t(
141
+ 'windward.integrations.components.external_integration.target'
142
+ ),
143
+ value: 'target',
144
+ },
145
+ {
146
+ text: this.$t(
147
+ 'windward.integrations.components.external_integration.version'
148
+ ),
149
+ value: 'version',
150
+ },
151
+ {
152
+ text: this.$t(
153
+ 'windward.integrations.components.external_integration.driver.scorm.download_package'
154
+ ),
155
+ value: 'download_package',
156
+ },
157
+ {
158
+ text: this.$t('shared.forms.enabled'),
159
+ value: 'enabled',
160
+ },
161
+ { text: this.$t('shared.forms.created'), value: 'created_at' },
162
+ {
163
+ text: this.$t('shared.forms.actions'),
164
+ value: 'actions',
165
+ sortable: false,
166
+ },
167
+ ],
168
+ isDownloading: false,
169
+ }
170
+ },
171
+
172
+ async fetch() {
173
+ if (
174
+ !this.$PermissionService.userHasAccessTo(
175
+ 'plugin.windward.integrations.course.externalIntegration',
176
+ 'readable'
177
+ ) ||
178
+ _.isEmpty(this.organization.id) ||
179
+ _.isEmpty(this.course.id)
180
+ ) {
181
+ // Display an angry error that they can't view this driver
182
+ this.$dialog.error(this.$t('shared.error.description_401'), {
183
+ duration: null,
184
+ action: {
185
+ text: this.$t('shared.forms.close'),
186
+ onClick: (_e, toastObject) => {
187
+ toastObject.goAway(0)
188
+ },
189
+ },
190
+ })
191
+ if (_.isEmpty(this.organization.id) || _.isEmpty(this.course.id)) {
192
+ // eslint-disable-next-line no-console
193
+ console.error(
194
+ 'Cannot load external integrations because organization or course is not set!'
195
+ )
196
+ } else {
197
+ // eslint-disable-next-line no-console
198
+ console.error(
199
+ 'You do not have access to this external integration!'
200
+ )
201
+ }
202
+
203
+ // Return so we don't even attempt loading
204
+ return false
205
+ }
206
+
207
+ await this.loadProviders()
208
+ },
209
+ computed: {
210
+ ...mapGetters({
211
+ organization: 'organization/get',
212
+ course: 'course/get',
213
+ }),
214
+ },
215
+ methods: {
216
+ onSaved() {
217
+ this.loadProviders()
218
+ },
219
+ async loadProviders() {
220
+ this.providers = await new Provider()
221
+ .for(
222
+ new Organization({ id: this.organization.id }),
223
+ new Course({ id: this.course.id })
224
+ )
225
+ .where('type', 'scorm')
226
+ .get()
227
+ },
228
+ onConfirmDelete(provider) {
229
+ this.$dialog.show(this.$t('shared.forms.confirm_delete_text'), {
230
+ icon: 'mdi-help',
231
+ duration: null,
232
+ action: [
233
+ {
234
+ text: this.$t('shared.forms.cancel'),
235
+ onClick: (_e, toastObject) => {
236
+ toastObject.goAway(0)
237
+ },
238
+ },
239
+ {
240
+ text: this.$t('shared.forms.confirm'),
241
+ // router navigation
242
+ onClick: (_e, toastObject) => {
243
+ this.deleteProvider(provider)
244
+ toastObject.goAway(0)
245
+ },
246
+ },
247
+ ],
248
+ })
249
+ },
250
+ async deleteProvider(provider) {
251
+ await provider.delete()
252
+ this.$dialog.success(this.$t('shared.response.deleted'))
253
+ // Reload providers now that we deleted one
254
+ this.loadProviders()
255
+ },
256
+ async onClickDownload(provider) {
257
+ this.isDownloading = true
258
+
259
+ try {
260
+ const result = await Provider.config({
261
+ method: 'GET',
262
+ })
263
+ .custom(
264
+ new Organization(this.organization),
265
+ new Course(this.course),
266
+ new Provider(provider),
267
+ 'download'
268
+ )
269
+ .file()
270
+
271
+ let filename = `${this.organization.name}_${this.course.name}_${provider.type}_${provider.version}_provider_${provider.id}`
272
+ // Replace all non letter/numbers with _
273
+ filename = filename.replaceAll(/[^a-zA-Z0-9\_\-]/gi, '_')
274
+ // Replace all double+ underscores with single
275
+ filename = filename.replaceAll(/_+/gi, '_')
276
+ filename += '.zip'
277
+
278
+ const url = URL.createObjectURL(result)
279
+ const link = document.createElement('a')
280
+ link.href = url
281
+ link.download = filename
282
+ link.click()
283
+ URL.revokeObjectURL(url)
284
+ } catch (e) {
285
+ console.error('Error getting download', e)
286
+ }
287
+
288
+ this.isDownloading = false
289
+ },
290
+ },
291
+ }
292
+ </script>
293
+
294
+ <style scoped>
295
+ .field--target {
296
+ max-width: 15em;
297
+ display: inline-block;
298
+ }
299
+ </style>
@@ -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>