@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escalated-dev/escalated",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "Vue 3 + Inertia.js UI components for Escalated — the embeddable support ticket system",
5
5
  "author": "Escalated Dev <hello@escalated.dev>",
6
6
  "license": "MIT",
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",
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>