@escalated-dev/escalated 0.8.0 → 0.11.1

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 (124) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +388 -361
  3. package/package.json +85 -84
  4. package/src/components/ActiveChatsPanel.vue +246 -246
  5. package/src/components/AdminDashboard.stories.js +1423 -1423
  6. package/src/components/AttachmentList.vue +65 -65
  7. package/src/components/ChatActionBar.vue +226 -226
  8. package/src/components/ChatBubble.vue +162 -162
  9. package/src/components/ChatComposer.vue +308 -308
  10. package/src/components/ChatQueue.vue +222 -222
  11. package/src/components/ChatThread.vue +197 -197
  12. package/src/components/CustomTicketActions.stories.js +71 -0
  13. package/src/components/CustomTicketActions.vue +101 -0
  14. package/src/components/EscalatedLayout.vue +628 -604
  15. package/src/components/FollowButton.vue +20 -1
  16. package/src/components/MentionDropdown.vue +154 -154
  17. package/src/components/MetricComparison.vue +89 -89
  18. package/src/components/PercentileChart.vue +67 -67
  19. package/src/components/PeriodSelector.vue +98 -98
  20. package/src/components/PresenceIndicator.vue +228 -213
  21. package/src/components/ReplyComposer.vue +336 -336
  22. package/src/components/ReplyThread.vue +153 -153
  23. package/src/components/ReportChart.vue +395 -395
  24. package/src/components/ReportExportButton.vue +108 -108
  25. package/src/components/SaveViewDialog.vue +172 -172
  26. package/src/components/SavedViewSidebar.vue +208 -208
  27. package/src/components/SkillTagManager.vue +3 -0
  28. package/src/components/SnoozeButton.vue +247 -247
  29. package/src/components/TicketFilters.vue +270 -270
  30. package/src/components/TicketList.vue +655 -655
  31. package/src/components/TicketSidebar.vue +4 -0
  32. package/src/components/TicketSplitDialog.vue +98 -98
  33. package/src/components/TicketSubjects.stories.js +63 -0
  34. package/src/components/TicketSubjects.vue +107 -0
  35. package/src/components/TwoFactorSetup.vue +1 -37
  36. package/src/components/VariableTokenInput.vue +111 -111
  37. package/src/components/WorkflowActionConfig.vue +287 -287
  38. package/src/components/WorkflowActionList.vue +350 -350
  39. package/src/components/WorkflowConditionBuilder.vue +265 -265
  40. package/src/components/WorkflowTriggerSelector.vue +125 -125
  41. package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
  42. package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
  43. package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
  44. package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
  45. package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
  46. package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
  47. package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
  48. package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
  49. package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
  50. package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
  51. package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
  52. package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
  53. package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
  54. package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
  55. package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
  56. package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
  57. package/src/composables/useChat.js +187 -187
  58. package/src/composables/useI18n.js +102 -102
  59. package/src/composables/useMentions.js +134 -134
  60. package/src/composables/usePermissions.js +31 -0
  61. package/src/composables/useRealtime.js +182 -182
  62. package/src/index.js +85 -84
  63. package/src/locales/README.md +13 -13
  64. package/src/locales/ar.json +727 -727
  65. package/src/locales/de.json +690 -690
  66. package/src/locales/en.json +889 -729
  67. package/src/locales/es.json +691 -691
  68. package/src/locales/fr.json +691 -691
  69. package/src/locales/index.js +16 -16
  70. package/src/locales/it.json +727 -727
  71. package/src/locales/ja.json +727 -727
  72. package/src/locales/ko.json +727 -727
  73. package/src/locales/nl.json +727 -727
  74. package/src/locales/pl.json +727 -727
  75. package/src/locales/pt-BR.json +727 -727
  76. package/src/locales/ru.json +727 -727
  77. package/src/locales/tr.json +727 -727
  78. package/src/locales/zh-CN.json +727 -727
  79. package/src/pages/Admin/Automations/Form.vue +280 -0
  80. package/src/pages/Admin/Automations/Index.vue +230 -0
  81. package/src/pages/Admin/Newsletters/Compose.vue +192 -0
  82. package/src/pages/Admin/Newsletters/Edit.vue +24 -0
  83. package/src/pages/Admin/Newsletters/Index.vue +141 -0
  84. package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
  85. package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
  86. package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
  87. package/src/pages/Admin/Newsletters/Settings.vue +85 -0
  88. package/src/pages/Admin/Newsletters/Show.vue +140 -0
  89. package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
  90. package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
  91. package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
  92. package/src/pages/Admin/Newsletters/usePreview.js +44 -0
  93. package/src/pages/Admin/Plugins/Index.vue +10 -2
  94. package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
  95. package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
  96. package/src/pages/Admin/Reports/Cohorts.vue +180 -180
  97. package/src/pages/Admin/Reports/Comparison.vue +141 -141
  98. package/src/pages/Admin/Reports/Dashboard.vue +226 -226
  99. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
  100. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
  101. package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
  102. package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
  103. package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
  104. package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
  105. package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
  106. package/src/pages/Admin/Settings.vue +1008 -1008
  107. package/src/pages/Admin/Skills/Form.vue +30 -10
  108. package/src/pages/Admin/Tickets/Index.vue +126 -126
  109. package/src/pages/Admin/Tickets/Show.vue +254 -249
  110. package/src/pages/Admin/Users/Index.vue +145 -145
  111. package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
  112. package/src/pages/Admin/Workflows/Builder.vue +207 -207
  113. package/src/pages/Admin/Workflows/Form.vue +24 -0
  114. package/src/pages/Admin/Workflows/Index.vue +235 -235
  115. package/src/pages/Admin/Workflows/Logs.vue +286 -286
  116. package/src/pages/Agent/TicketIndex.vue +112 -112
  117. package/src/pages/Agent/TicketShow.vue +547 -494
  118. package/src/pages/Customer/Index.vue +56 -56
  119. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
  120. package/src/pages/Plugin/Page.vue +61 -0
  121. package/src/plugin.js +175 -175
  122. package/src/utils/mentionHighlight.js +24 -24
  123. package/src/widget/EscalatedWidget.vue +804 -804
  124. package/src/widget/index.js +254 -254
@@ -0,0 +1,163 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm, Link } from '@inertiajs/vue3';
4
+ import { computed } from 'vue';
5
+
6
+ /**
7
+ * What happens to a ticket raised by someone who is not signed in.
8
+ *
9
+ * The three modes are the guest policy: leave it unattached, attach it to a
10
+ * standing guest account, or ask the person to sign up before it is accepted.
11
+ * Each backend stores these as three settings, and the second and third each
12
+ * need one value alongside the mode.
13
+ */
14
+ const props = defineProps({
15
+ settings: { type: Object, default: () => ({}) },
16
+ });
17
+
18
+ const MODES = [
19
+ {
20
+ value: 'unassigned',
21
+ label: 'Leave unattached',
22
+ description: 'The ticket is stored with the guest name and email and no user behind it.',
23
+ },
24
+ {
25
+ value: 'guest_user',
26
+ label: 'Attach to a guest account',
27
+ description: 'Every guest ticket is owned by one standing account, so agents see a consistent requester.',
28
+ },
29
+ {
30
+ value: 'prompt_signup',
31
+ label: 'Ask the guest to sign up',
32
+ description: 'The guest is sent to your sign-up page before the ticket is accepted.',
33
+ },
34
+ ];
35
+
36
+ const form = useForm({
37
+ guest_policy_mode: props.settings?.guest_policy_mode ?? 'unassigned',
38
+ guest_policy_user_id: props.settings?.guest_policy_user_id ?? '',
39
+ guest_policy_signup_url_template: props.settings?.guest_policy_signup_url_template ?? '',
40
+ });
41
+
42
+ const needsUserId = computed(() => form.guest_policy_mode === 'guest_user');
43
+ const needsSignupUrl = computed(() => form.guest_policy_mode === 'prompt_signup');
44
+
45
+ function submit() {
46
+ form.post(route('escalated.admin.settings.public-tickets.update'));
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <EscalatedLayout title="Public Tickets">
52
+ <div class="mx-auto max-w-2xl">
53
+ <div class="mb-6">
54
+ <Link
55
+ :href="route('escalated.admin.settings')"
56
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
57
+ >
58
+ &larr; Back to Settings
59
+ </Link>
60
+ </div>
61
+
62
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
63
+ <h2 class="text-lg font-semibold text-[var(--esc-panel-text-secondary)]">Public Tickets</h2>
64
+ <p class="mt-1 text-sm text-[var(--esc-panel-text-muted)]">
65
+ How a ticket raised by someone who is not signed in is recorded.
66
+ </p>
67
+
68
+ <form class="mt-6 space-y-6" @submit.prevent="submit">
69
+ <fieldset class="space-y-3">
70
+ <legend class="mb-1.5 text-sm font-medium text-[var(--esc-panel-text-secondary)]">
71
+ Guest policy
72
+ </legend>
73
+
74
+ <label
75
+ v-for="mode in MODES"
76
+ :key="mode.value"
77
+ class="flex cursor-pointer gap-3 rounded-lg border p-3 transition-colors"
78
+ :class="
79
+ form.guest_policy_mode === mode.value
80
+ ? 'border-[var(--esc-panel-accent)]/40 bg-[var(--esc-panel-hover)]'
81
+ : 'border-[var(--esc-panel-border)] hover:bg-[var(--esc-panel-hover)]'
82
+ "
83
+ >
84
+ <input
85
+ v-model="form.guest_policy_mode"
86
+ type="radio"
87
+ name="guest_policy_mode"
88
+ :value="mode.value"
89
+ class="mt-1"
90
+ />
91
+ <span>
92
+ <span class="block text-sm text-[var(--esc-panel-text-secondary)]">
93
+ {{ mode.label }}
94
+ </span>
95
+ <span class="block text-xs text-[var(--esc-panel-text-muted)]">
96
+ {{ mode.description }}
97
+ </span>
98
+ </span>
99
+ </label>
100
+
101
+ <p v-if="form.errors.guest_policy_mode" class="text-xs text-red-400">
102
+ {{ form.errors.guest_policy_mode }}
103
+ </p>
104
+ </fieldset>
105
+
106
+ <div v-if="needsUserId">
107
+ <label
108
+ class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
109
+ for="guest-user-id"
110
+ >
111
+ Guest account user ID
112
+ </label>
113
+ <input
114
+ id="guest-user-id"
115
+ v-model="form.guest_policy_user_id"
116
+ type="number"
117
+ min="1"
118
+ required
119
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-accent)]/40 focus:outline-none"
120
+ />
121
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
122
+ The id of an existing user in your application. Every guest ticket is attached to it.
123
+ </p>
124
+ <p v-if="form.errors.guest_policy_user_id" class="mt-1 text-xs text-red-400">
125
+ {{ form.errors.guest_policy_user_id }}
126
+ </p>
127
+ </div>
128
+
129
+ <div v-if="needsSignupUrl">
130
+ <label
131
+ class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
132
+ for="signup-url"
133
+ >
134
+ Sign-up URL
135
+ </label>
136
+ <input
137
+ id="signup-url"
138
+ v-model="form.guest_policy_signup_url_template"
139
+ type="text"
140
+ maxlength="500"
141
+ placeholder="/register?email={email}"
142
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-accent)]/40 focus:outline-none"
143
+ />
144
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
145
+ <code>{email}</code> is replaced with the address the guest entered.
146
+ </p>
147
+ <p v-if="form.errors.guest_policy_signup_url_template" class="mt-1 text-xs text-red-400">
148
+ {{ form.errors.guest_policy_signup_url_template }}
149
+ </p>
150
+ </div>
151
+
152
+ <button
153
+ type="submit"
154
+ :disabled="form.processing"
155
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-2 text-sm font-medium text-white transition-all hover:from-[var(--esc-panel-accent-hover)] hover:to-[var(--esc-panel-accent-secondary-hover)] disabled:opacity-50"
156
+ >
157
+ Save settings
158
+ </button>
159
+ </form>
160
+ </div>
161
+ </div>
162
+ </EscalatedLayout>
163
+ </template>
@@ -8,14 +8,34 @@ const form = useForm({
8
8
  sso_provider: props.settings.sso_provider ?? 'none',
9
9
  sso_entity_id: props.settings.sso_entity_id ?? '',
10
10
  sso_url: props.settings.sso_url ?? '',
11
+ sso_login_url: props.settings.sso_login_url ?? props.settings.sso_url ?? '',
12
+ sso_logout_url: props.settings.sso_logout_url ?? '',
13
+ sso_metadata_url: props.settings.sso_metadata_url ?? '',
11
14
  sso_certificate: props.settings.sso_certificate ?? '',
12
15
  sso_attr_email: props.settings.sso_attr_email ?? 'email',
13
16
  sso_attr_name: props.settings.sso_attr_name ?? 'name',
14
17
  sso_attr_role: props.settings.sso_attr_role ?? 'role',
15
18
  sso_jwt_secret: props.settings.sso_jwt_secret ?? '',
16
19
  sso_jwt_algorithm: props.settings.sso_jwt_algorithm ?? 'HS256',
20
+ sso_oauth_authorize_url: props.settings.sso_oauth_authorize_url ?? '',
21
+ sso_oauth_token_url: props.settings.sso_oauth_token_url ?? '',
22
+ sso_oauth_userinfo_url: props.settings.sso_oauth_userinfo_url ?? '',
23
+ sso_oauth_client_id: props.settings.sso_oauth_client_id ?? '',
24
+ sso_oauth_client_secret: props.settings.sso_oauth_client_secret ?? '',
25
+ sso_oauth_scopes: props.settings.sso_oauth_scopes ?? 'openid profile email',
17
26
  });
18
27
 
28
+ function readCertificate(event) {
29
+ const [file] = event.target.files ?? [];
30
+ if (!file) return;
31
+
32
+ const reader = new window.FileReader();
33
+ reader.onload = () => {
34
+ form.sso_certificate = typeof reader.result === 'string' ? reader.result : '';
35
+ };
36
+ reader.readAsText(file);
37
+ }
38
+
19
39
  function submit() {
20
40
  form.post(route('escalated.admin.settings.sso.update'));
21
41
  }
@@ -42,6 +62,7 @@ function submit() {
42
62
  <option value="none">None (disabled)</option>
43
63
  <option value="saml">SAML 2.0</option>
44
64
  <option value="jwt">JWT</option>
65
+ <option value="oauth">OAuth 2.0 / OIDC</option>
45
66
  </select>
46
67
  </div>
47
68
  </div>
@@ -72,16 +93,48 @@ function submit() {
72
93
  </div>
73
94
  </div>
74
95
  <div>
75
- <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">SSO URL</label>
96
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
97
+ >Login URL</label
98
+ >
76
99
  <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">The Single Sign-On URL endpoint</p>
77
100
  <input
78
- v-model="form.sso_url"
101
+ v-model="form.sso_login_url"
79
102
  type="url"
80
103
  placeholder="https://idp.example.com/sso"
81
104
  class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
82
105
  />
83
- <div v-if="form.errors.sso_url" class="mt-1 text-sm text-rose-400">
84
- {{ form.errors.sso_url }}
106
+ <div v-if="form.errors.sso_login_url" class="mt-1 text-sm text-rose-400">
107
+ {{ form.errors.sso_login_url }}
108
+ </div>
109
+ </div>
110
+ <div class="grid gap-5 md:grid-cols-2">
111
+ <div>
112
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
113
+ >Logout URL</label
114
+ >
115
+ <input
116
+ v-model="form.sso_logout_url"
117
+ type="url"
118
+ placeholder="https://idp.example.com/logout"
119
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
120
+ />
121
+ <div v-if="form.errors.sso_logout_url" class="mt-1 text-sm text-rose-400">
122
+ {{ form.errors.sso_logout_url }}
123
+ </div>
124
+ </div>
125
+ <div>
126
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
127
+ >Metadata URL</label
128
+ >
129
+ <input
130
+ v-model="form.sso_metadata_url"
131
+ type="url"
132
+ placeholder="https://idp.example.com/metadata"
133
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
134
+ />
135
+ <div v-if="form.errors.sso_metadata_url" class="mt-1 text-sm text-rose-400">
136
+ {{ form.errors.sso_metadata_url }}
137
+ </div>
85
138
  </div>
86
139
  </div>
87
140
  <div>
@@ -91,6 +144,12 @@ function submit() {
91
144
  <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
92
145
  X.509 certificate from your Identity Provider (PEM format)
93
146
  </p>
147
+ <input
148
+ type="file"
149
+ accept=".pem,.crt,.cer,.txt"
150
+ class="mt-2 block text-xs text-[var(--esc-panel-text-muted)]"
151
+ @change="readCertificate"
152
+ />
94
153
  <textarea
95
154
  v-model="form.sso_certificate"
96
155
  rows="6"
@@ -139,6 +198,8 @@ function submit() {
139
198
  <option value="HS384">HS384</option>
140
199
  <option value="HS512">HS512</option>
141
200
  <option value="RS256">RS256</option>
201
+ <option value="RS384">RS384</option>
202
+ <option value="RS512">RS512</option>
142
203
  </select>
143
204
  </div>
144
205
  <div>
@@ -156,6 +217,98 @@ function submit() {
156
217
  </div>
157
218
  </div>
158
219
 
220
+ <div
221
+ v-if="form.sso_provider === 'oauth'"
222
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
223
+ >
224
+ <h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">OAuth / OIDC Configuration</h3>
225
+ <div class="space-y-5">
226
+ <div class="grid gap-5 md:grid-cols-2">
227
+ <div>
228
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
229
+ >Authorize URL</label
230
+ >
231
+ <input
232
+ v-model="form.sso_oauth_authorize_url"
233
+ type="url"
234
+ placeholder="https://idp.example.com/oauth/authorize"
235
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
236
+ />
237
+ <div v-if="form.errors.sso_oauth_authorize_url" class="mt-1 text-sm text-rose-400">
238
+ {{ form.errors.sso_oauth_authorize_url }}
239
+ </div>
240
+ </div>
241
+ <div>
242
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
243
+ >Token URL</label
244
+ >
245
+ <input
246
+ v-model="form.sso_oauth_token_url"
247
+ type="url"
248
+ placeholder="https://idp.example.com/oauth/token"
249
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
250
+ />
251
+ <div v-if="form.errors.sso_oauth_token_url" class="mt-1 text-sm text-rose-400">
252
+ {{ form.errors.sso_oauth_token_url }}
253
+ </div>
254
+ </div>
255
+ </div>
256
+ <div class="grid gap-5 md:grid-cols-2">
257
+ <div>
258
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
259
+ >Userinfo URL</label
260
+ >
261
+ <input
262
+ v-model="form.sso_oauth_userinfo_url"
263
+ type="url"
264
+ placeholder="https://idp.example.com/oauth/userinfo"
265
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
266
+ />
267
+ <div v-if="form.errors.sso_oauth_userinfo_url" class="mt-1 text-sm text-rose-400">
268
+ {{ form.errors.sso_oauth_userinfo_url }}
269
+ </div>
270
+ </div>
271
+ <div>
272
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
273
+ >Scopes</label
274
+ >
275
+ <input
276
+ v-model="form.sso_oauth_scopes"
277
+ type="text"
278
+ placeholder="openid profile email"
279
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
280
+ />
281
+ </div>
282
+ </div>
283
+ <div class="grid gap-5 md:grid-cols-2">
284
+ <div>
285
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
286
+ >Client ID</label
287
+ >
288
+ <input
289
+ v-model="form.sso_oauth_client_id"
290
+ type="text"
291
+ placeholder="escalated-admin"
292
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
293
+ />
294
+ <div v-if="form.errors.sso_oauth_client_id" class="mt-1 text-sm text-rose-400">
295
+ {{ form.errors.sso_oauth_client_id }}
296
+ </div>
297
+ </div>
298
+ <div>
299
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
300
+ >Client Secret</label
301
+ >
302
+ <input
303
+ v-model="form.sso_oauth_client_secret"
304
+ type="password"
305
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
306
+ />
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
159
312
  <!-- Attribute Mapping -->
160
313
  <div
161
314
  v-if="form.sso_provider !== 'none'"
@@ -12,6 +12,7 @@ defineProps({
12
12
  const page = usePage();
13
13
  const showSetup = ref(false);
14
14
  const setupData = computed(() => page.props.flash?.two_factor_setup);
15
+ const confirmedData = computed(() => page.props.flash?.two_factor_confirmed);
15
16
 
16
17
  function startSetup() {
17
18
  router.post(
@@ -98,12 +99,37 @@ function disable() {
98
99
  v-if="setupData && !enabled"
99
100
  class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
100
101
  >
101
- <TwoFactorSetup
102
- :qr-uri="setupData.qr_uri"
103
- :recovery-codes="setupData.recovery_codes"
104
- @confirm="confirmCode"
105
- @cancel="showSetup = false"
106
- />
102
+ <TwoFactorSetup :qr-uri="setupData.qr_uri" @confirm="confirmCode" @cancel="showSetup = false" />
103
+ </div>
104
+
105
+ <div
106
+ v-if="confirmedData?.recovery_codes?.length"
107
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
108
+ >
109
+ <div class="flex items-start justify-between gap-4">
110
+ <div>
111
+ <h3 class="text-sm font-semibold text-[var(--esc-panel-text)]">Recovery Codes</h3>
112
+ <p class="mt-2 text-sm text-[var(--esc-panel-text-tertiary)]">
113
+ Save these codes now. Each one can be used once if you lose access to your authenticator.
114
+ </p>
115
+ </div>
116
+ <button
117
+ type="button"
118
+ class="rounded-lg border border-[var(--esc-panel-border)] px-4 py-2 text-sm text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-surface-alt)]"
119
+ @click="navigator.clipboard?.writeText(confirmedData.recovery_codes.join('\n'))"
120
+ >
121
+ Copy Codes
122
+ </button>
123
+ </div>
124
+ <div class="mt-4 grid gap-2 md:grid-cols-2">
125
+ <code
126
+ v-for="code in confirmedData.recovery_codes"
127
+ :key="code"
128
+ class="rounded-lg bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-center text-sm font-mono text-[var(--esc-panel-text-secondary)]"
129
+ >
130
+ {{ code }}
131
+ </code>
132
+ </div>
107
133
  </div>
108
134
  </div>
109
135
  </EscalatedLayout>