@escalated-dev/escalated 0.11.3 → 0.11.5
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/package.json +1 -1
- package/pages.json +3 -1
- package/src/pages/Auth/TwoFactorChallenge.vue +117 -0
- package/src/pages/Error.vue +69 -0
package/package.json
CHANGED
package/pages.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "Generated by scripts/generate-page-manifest.js from src/pages. Every name here resolves to a component; a name a backend renders that is not here is a blank screen.",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"pages": [
|
|
5
5
|
"Escalated/Admin/ApiTokens/Index",
|
|
6
6
|
"Escalated/Admin/AuditLog/Index",
|
|
@@ -83,11 +83,13 @@
|
|
|
83
83
|
"Escalated/Agent/Dashboard",
|
|
84
84
|
"Escalated/Agent/TicketIndex",
|
|
85
85
|
"Escalated/Agent/TicketShow",
|
|
86
|
+
"Escalated/Auth/TwoFactorChallenge",
|
|
86
87
|
"Escalated/Customer/Create",
|
|
87
88
|
"Escalated/Customer/Index",
|
|
88
89
|
"Escalated/Customer/KnowledgeBase/Article",
|
|
89
90
|
"Escalated/Customer/KnowledgeBase/Index",
|
|
90
91
|
"Escalated/Customer/Show",
|
|
92
|
+
"Escalated/Error",
|
|
91
93
|
"Escalated/Guest/Create",
|
|
92
94
|
"Escalated/Guest/Show",
|
|
93
95
|
"Escalated/Plugin/Page"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useForm } from '@inertiajs/vue3';
|
|
3
|
+
import { computed, ref } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The post-login two-factor challenge.
|
|
7
|
+
*
|
|
8
|
+
* A backend renders this after authenticating a password but before letting
|
|
9
|
+
* someone into the panel, so it deliberately does not use EscalatedLayout —
|
|
10
|
+
* that layout is the signed-in chrome, and at this point the sign-in is not
|
|
11
|
+
* finished. Nothing here should suggest access that has not been granted yet.
|
|
12
|
+
*
|
|
13
|
+
* Both a TOTP code and a recovery code post to the same endpoint; the backend
|
|
14
|
+
* reads whichever field arrived.
|
|
15
|
+
*/
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
// The route to post to. A host mounts Escalated under its own prefix, and
|
|
18
|
+
// this page is reached before any layout has resolved one.
|
|
19
|
+
action: { type: String, default: '' },
|
|
20
|
+
error: { type: String, default: '' },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const useRecoveryCode = ref(false);
|
|
24
|
+
|
|
25
|
+
const form = useForm({
|
|
26
|
+
code: '',
|
|
27
|
+
recovery_code: '',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const endpoint = computed(() => props.action || route('escalated.two-factor.verify'));
|
|
31
|
+
|
|
32
|
+
function submit() {
|
|
33
|
+
// Only the field in use is sent, so a blank one cannot be mistaken for an
|
|
34
|
+
// attempt with an empty code.
|
|
35
|
+
form.transform((data) =>
|
|
36
|
+
useRecoveryCode.value ? { recovery_code: data.recovery_code } : { code: data.code },
|
|
37
|
+
).post(endpoint.value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toggleMode() {
|
|
41
|
+
useRecoveryCode.value = !useRecoveryCode.value;
|
|
42
|
+
form.clearErrors();
|
|
43
|
+
form.code = '';
|
|
44
|
+
form.recovery_code = '';
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="flex min-h-screen items-center justify-center bg-[var(--esc-panel-bg)] px-4">
|
|
50
|
+
<div class="w-full max-w-sm">
|
|
51
|
+
<h1 class="text-lg font-semibold text-[var(--esc-panel-text-secondary)]">Two-factor authentication</h1>
|
|
52
|
+
<p class="mt-1 text-sm text-[var(--esc-panel-text-muted)]">
|
|
53
|
+
{{
|
|
54
|
+
useRecoveryCode
|
|
55
|
+
? 'Enter one of the recovery codes you saved when you set this up.'
|
|
56
|
+
: 'Enter the six-digit code from your authenticator app.'
|
|
57
|
+
}}
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
<form
|
|
61
|
+
class="mt-6 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5"
|
|
62
|
+
@submit.prevent="submit"
|
|
63
|
+
>
|
|
64
|
+
<div v-if="!useRecoveryCode">
|
|
65
|
+
<label class="block text-xs font-medium text-[var(--esc-panel-text-muted)]" for="totp-code">
|
|
66
|
+
Authentication code
|
|
67
|
+
</label>
|
|
68
|
+
<input
|
|
69
|
+
id="totp-code"
|
|
70
|
+
v-model="form.code"
|
|
71
|
+
type="text"
|
|
72
|
+
inputmode="numeric"
|
|
73
|
+
autocomplete="one-time-code"
|
|
74
|
+
maxlength="6"
|
|
75
|
+
autofocus
|
|
76
|
+
class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-center text-lg tracking-[0.4em] text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-accent)] focus:outline-none"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div v-else>
|
|
81
|
+
<label class="block text-xs font-medium text-[var(--esc-panel-text-muted)]" for="recovery-code">
|
|
82
|
+
Recovery code
|
|
83
|
+
</label>
|
|
84
|
+
<input
|
|
85
|
+
id="recovery-code"
|
|
86
|
+
v-model="form.recovery_code"
|
|
87
|
+
type="text"
|
|
88
|
+
autocomplete="off"
|
|
89
|
+
autofocus
|
|
90
|
+
class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-accent)] focus:outline-none"
|
|
91
|
+
/>
|
|
92
|
+
<p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">Each recovery code works once.</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<p v-if="error || form.errors.code || form.errors.recovery_code" class="mt-2 text-xs text-red-400">
|
|
96
|
+
{{ error || form.errors.code || form.errors.recovery_code }}
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
<button
|
|
100
|
+
type="submit"
|
|
101
|
+
:disabled="form.processing"
|
|
102
|
+
class="mt-4 w-full 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"
|
|
103
|
+
>
|
|
104
|
+
Verify
|
|
105
|
+
</button>
|
|
106
|
+
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
class="mt-3 w-full text-xs text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-text-secondary)]"
|
|
110
|
+
@click="toggleMode"
|
|
111
|
+
>
|
|
112
|
+
{{ useRecoveryCode ? 'Use an authenticator code instead' : 'Use a recovery code instead' }}
|
|
113
|
+
</button>
|
|
114
|
+
</form>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Link } from '@inertiajs/vue3';
|
|
3
|
+
import { computed } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The refusal and not-found screen.
|
|
7
|
+
*
|
|
8
|
+
* Backends render this instead of an HTML error page so a 403 or a 404 inside
|
|
9
|
+
* the panel stays inside the panel. It deliberately does not use
|
|
10
|
+
* EscalatedLayout: the most common reason to arrive here is that the viewer was
|
|
11
|
+
* refused, and the layout's navigation is a list of places they may equally not
|
|
12
|
+
* be allowed to go. Rendering it would offer a menu that mostly leads back
|
|
13
|
+
* here.
|
|
14
|
+
*
|
|
15
|
+
* The message comes from the backend, already translated. The headings below
|
|
16
|
+
* are the fallback for a status arriving with no message at all.
|
|
17
|
+
*/
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
status: { type: [Number, String], default: 500 },
|
|
20
|
+
message: { type: String, default: '' },
|
|
21
|
+
|
|
22
|
+
// Where "back" goes. A host mounts Escalated under its own prefix, and this
|
|
23
|
+
// page is reached without a layout to resolve one.
|
|
24
|
+
home: { type: String, default: '' },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const code = computed(() => String(props.status));
|
|
28
|
+
|
|
29
|
+
const title = computed(
|
|
30
|
+
() =>
|
|
31
|
+
({
|
|
32
|
+
403: 'Not allowed',
|
|
33
|
+
404: 'Not found',
|
|
34
|
+
419: 'Session expired',
|
|
35
|
+
429: 'Too many requests',
|
|
36
|
+
500: 'Something went wrong',
|
|
37
|
+
503: 'Temporarily unavailable',
|
|
38
|
+
})[code.value] || 'Something went wrong',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const explanation = computed(
|
|
42
|
+
() =>
|
|
43
|
+
({
|
|
44
|
+
403: 'Your account does not have access to this.',
|
|
45
|
+
404: 'That page does not exist, or it has been removed.',
|
|
46
|
+
419: 'Your session expired. Sign in again to continue.',
|
|
47
|
+
429: 'Too many requests in a short space of time. Wait a moment and try again.',
|
|
48
|
+
503: 'The service is down for maintenance. Try again shortly.',
|
|
49
|
+
})[code.value] || 'The request could not be completed.',
|
|
50
|
+
);
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div class="flex min-h-screen items-center justify-center bg-[var(--esc-panel-bg)] px-4">
|
|
55
|
+
<div class="w-full max-w-md text-center">
|
|
56
|
+
<p class="font-mono text-sm tracking-widest text-[var(--esc-panel-text-muted)]">{{ code }}</p>
|
|
57
|
+
<h1 class="mt-2 text-xl font-semibold text-[var(--esc-panel-text-secondary)]">{{ title }}</h1>
|
|
58
|
+
<p class="mt-3 text-sm text-[var(--esc-panel-text-muted)]">{{ message || explanation }}</p>
|
|
59
|
+
|
|
60
|
+
<Link
|
|
61
|
+
v-if="home"
|
|
62
|
+
:href="home"
|
|
63
|
+
class="mt-6 inline-block rounded-lg border border-[var(--esc-panel-border)] px-4 py-2 text-sm text-[var(--esc-panel-text-tertiary)] transition-colors hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
64
|
+
>
|
|
65
|
+
Back to the panel
|
|
66
|
+
</Link>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|