@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.
- package/LICENSE +21 -21
- package/README.md +388 -361
- package/package.json +85 -84
- package/src/components/ActiveChatsPanel.vue +246 -246
- package/src/components/AdminDashboard.stories.js +1423 -1423
- package/src/components/AttachmentList.vue +65 -65
- package/src/components/ChatActionBar.vue +226 -226
- package/src/components/ChatBubble.vue +162 -162
- package/src/components/ChatComposer.vue +308 -308
- package/src/components/ChatQueue.vue +222 -222
- package/src/components/ChatThread.vue +197 -197
- package/src/components/CustomTicketActions.stories.js +71 -0
- package/src/components/CustomTicketActions.vue +101 -0
- package/src/components/EscalatedLayout.vue +628 -604
- package/src/components/FollowButton.vue +20 -1
- package/src/components/MentionDropdown.vue +154 -154
- package/src/components/MetricComparison.vue +89 -89
- package/src/components/PercentileChart.vue +67 -67
- package/src/components/PeriodSelector.vue +98 -98
- package/src/components/PresenceIndicator.vue +228 -213
- package/src/components/ReplyComposer.vue +336 -336
- package/src/components/ReplyThread.vue +153 -153
- package/src/components/ReportChart.vue +395 -395
- package/src/components/ReportExportButton.vue +108 -108
- package/src/components/SaveViewDialog.vue +172 -172
- package/src/components/SavedViewSidebar.vue +208 -208
- package/src/components/SkillTagManager.vue +3 -0
- package/src/components/SnoozeButton.vue +247 -247
- package/src/components/TicketFilters.vue +270 -270
- package/src/components/TicketList.vue +655 -655
- package/src/components/TicketSidebar.vue +4 -0
- package/src/components/TicketSplitDialog.vue +98 -98
- package/src/components/TicketSubjects.stories.js +63 -0
- package/src/components/TicketSubjects.vue +107 -0
- package/src/components/TwoFactorSetup.vue +1 -37
- package/src/components/VariableTokenInput.vue +111 -111
- package/src/components/WorkflowActionConfig.vue +287 -287
- package/src/components/WorkflowActionList.vue +350 -350
- package/src/components/WorkflowConditionBuilder.vue +265 -265
- package/src/components/WorkflowTriggerSelector.vue +125 -125
- package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
- package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
- package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
- package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
- package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
- package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
- package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
- package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
- package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
- package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
- package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
- package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
- package/src/composables/useChat.js +187 -187
- package/src/composables/useI18n.js +102 -102
- package/src/composables/useMentions.js +134 -134
- package/src/composables/usePermissions.js +31 -0
- package/src/composables/useRealtime.js +182 -182
- package/src/index.js +85 -84
- package/src/locales/README.md +13 -13
- package/src/locales/ar.json +727 -727
- package/src/locales/de.json +690 -690
- package/src/locales/en.json +889 -729
- package/src/locales/es.json +691 -691
- package/src/locales/fr.json +691 -691
- package/src/locales/index.js +16 -16
- package/src/locales/it.json +727 -727
- package/src/locales/ja.json +727 -727
- package/src/locales/ko.json +727 -727
- package/src/locales/nl.json +727 -727
- package/src/locales/pl.json +727 -727
- package/src/locales/pt-BR.json +727 -727
- package/src/locales/ru.json +727 -727
- package/src/locales/tr.json +727 -727
- package/src/locales/zh-CN.json +727 -727
- package/src/pages/Admin/Automations/Form.vue +280 -0
- package/src/pages/Admin/Automations/Index.vue +230 -0
- package/src/pages/Admin/Newsletters/Compose.vue +192 -0
- package/src/pages/Admin/Newsletters/Edit.vue +24 -0
- package/src/pages/Admin/Newsletters/Index.vue +141 -0
- package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
- package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
- package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
- package/src/pages/Admin/Newsletters/Settings.vue +85 -0
- package/src/pages/Admin/Newsletters/Show.vue +140 -0
- package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
- package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
- package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
- package/src/pages/Admin/Newsletters/usePreview.js +44 -0
- package/src/pages/Admin/Plugins/Index.vue +10 -2
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
- package/src/pages/Admin/Reports/Cohorts.vue +180 -180
- package/src/pages/Admin/Reports/Comparison.vue +141 -141
- package/src/pages/Admin/Reports/Dashboard.vue +226 -226
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
- package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
- package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
- package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
- package/src/pages/Admin/Settings.vue +1008 -1008
- package/src/pages/Admin/Skills/Form.vue +30 -10
- package/src/pages/Admin/Tickets/Index.vue +126 -126
- package/src/pages/Admin/Tickets/Show.vue +254 -249
- package/src/pages/Admin/Users/Index.vue +145 -145
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
- package/src/pages/Admin/Workflows/Builder.vue +207 -207
- package/src/pages/Admin/Workflows/Form.vue +24 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -235
- package/src/pages/Admin/Workflows/Logs.vue +286 -286
- package/src/pages/Agent/TicketIndex.vue +112 -112
- package/src/pages/Agent/TicketShow.vue +547 -494
- package/src/pages/Customer/Index.vue +56 -56
- package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
- package/src/pages/Plugin/Page.vue +61 -0
- package/src/plugin.js +175 -175
- package/src/utils/mentionHighlight.js +24 -24
- package/src/widget/EscalatedWidget.vue +804 -804
- 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
|
+
← 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)]"
|
|
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.
|
|
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.
|
|
84
|
-
{{ form.errors.
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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>
|