@webx-ui/module-auth 0.0.1 → 0.2.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.
- package/LICENSE +21 -21
- package/README.md +12 -9
- package/dist/LoginCard.d.ts +9 -1
- package/dist/UserMenu.d.ts +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +219 -148
- package/dist/index.js.map +1 -1
- package/dist/messages.d.ts +9 -0
- package/dist/session.d.ts +8 -0
- package/dist/style.css +1 -1
- package/package.json +3 -3
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 webx-ui
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 webx-ui
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -49,19 +49,21 @@ What it does carry: a password reveal, a Caps Lock warning, `autocomplete="usern
|
|
|
49
49
|
`current-password` so password managers work, validation errors landing on the right field, and
|
|
50
50
|
a countdown when the server throttles.
|
|
51
51
|
|
|
52
|
-
Every string is a prop
|
|
52
|
+
Every string is a prop, and a prop given wins. What happens when one is not given is that the
|
|
53
|
+
label comes from the panel's dictionary — assembled by the server from the same `lang` files it
|
|
54
|
+
writes its own messages from, so the card and the 422 under its fields speak one language
|
|
55
|
+
without anybody listing labels twice.
|
|
53
56
|
|
|
54
57
|
```ts
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
rememberLabel: 'Запам’ятати мене',
|
|
61
|
-
},
|
|
62
|
-
})
|
|
58
|
+
// Nothing to pass: a panel in Ukrainian draws a Ukrainian card.
|
|
59
|
+
auth()
|
|
60
|
+
|
|
61
|
+
// Unless this particular panel wants its own words.
|
|
62
|
+
auth({ card: { submitLabel: 'Увійти' } })
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Placed outside a panel, with no server to ask, the card falls back to the English it ships.
|
|
66
|
+
|
|
65
67
|
`WxLoginCard` is exported for a panel that wants to place it itself.
|
|
66
68
|
|
|
67
69
|
## The session
|
|
@@ -74,6 +76,7 @@ const auth = useAuth()
|
|
|
74
76
|
await auth.login({ email, password, remember: true })
|
|
75
77
|
await auth.logout()
|
|
76
78
|
const user = await auth.me() // null when nobody is
|
|
79
|
+
await auth.setLocale('uk') // the language this administrator reads the panel in
|
|
77
80
|
```
|
|
78
81
|
|
|
79
82
|
There is nothing kept here: the session lives in a cookie, the server is the source of truth,
|
package/dist/LoginCard.d.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* little as possible to somebody who has no business behind it, and the people who do have
|
|
6
6
|
* business behind it already know where they are.
|
|
7
7
|
*
|
|
8
|
-
* Every string is a prop
|
|
8
|
+
* Every string is still a prop and a prop given still wins. What changed is what happens when
|
|
9
|
+
* one is not: the label comes from the panel's dictionary, which the server assembles from the
|
|
10
|
+
* same `lang` files it answers its own messages from. A panel gets its language without being
|
|
11
|
+
* told every label twice, and a card placed outside a panel still speaks English.
|
|
9
12
|
*/
|
|
10
13
|
type __VLS_Props = {
|
|
11
14
|
emailLabel?: string;
|
|
@@ -13,7 +16,10 @@ type __VLS_Props = {
|
|
|
13
16
|
rememberLabel?: string;
|
|
14
17
|
submitLabel?: string;
|
|
15
18
|
capsLockWarning?: string;
|
|
19
|
+
/** `:seconds` is replaced with however long the server said to wait. */
|
|
16
20
|
throttleMessage?: string;
|
|
21
|
+
revealLabel?: string;
|
|
22
|
+
hideLabel?: string;
|
|
17
23
|
/** Offer to stay signed in. The server honours it with a long-lived cookie. */
|
|
18
24
|
remember?: boolean;
|
|
19
25
|
/** Put the cursor in the email field. Off for a card that is not the only thing on screen. */
|
|
@@ -32,6 +38,8 @@ declare const _default: import('vue').DefineComponent<__VLS_Props, {
|
|
|
32
38
|
submitLabel: string;
|
|
33
39
|
capsLockWarning: string;
|
|
34
40
|
throttleMessage: string;
|
|
41
|
+
revealLabel: string;
|
|
42
|
+
hideLabel: string;
|
|
35
43
|
remember: boolean;
|
|
36
44
|
autofocus: boolean;
|
|
37
45
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
|
package/dist/UserMenu.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The corner of the header: who this is, and the way
|
|
2
|
+
* The corner of the header: who this is, which language they read the panel in, and the way
|
|
3
|
+
* out.
|
|
4
|
+
*
|
|
5
|
+
* The language picker lives here rather than in a settings screen because it is a property of
|
|
6
|
+
* the person, not of the site — and because somebody who has landed in a language they cannot
|
|
7
|
+
* read needs it within reach, not three clicks into a section they cannot navigate.
|
|
3
8
|
*/
|
|
4
9
|
type __VLS_Props = {
|
|
5
10
|
signOutLabel?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -17,3 +17,4 @@ export { createAuthSession, provideAuth, useAuth, authKey } from './session';
|
|
|
17
17
|
export type { AuthSession, Credentials } from './session';
|
|
18
18
|
export { default as WxLoginCard } from './LoginCard';
|
|
19
19
|
export { default as WxUserMenu } from './UserMenu';
|
|
20
|
+
export { authMessages } from './messages';
|
package/dist/index.js
CHANGED
|
@@ -1,209 +1,230 @@
|
|
|
1
|
-
import { inject as
|
|
2
|
-
import { HttpError as
|
|
3
|
-
const
|
|
4
|
-
function
|
|
5
|
-
const e =
|
|
1
|
+
import { inject as ee, defineComponent as q, computed as C, ref as b, useTemplateRef as te, onBeforeUnmount as ae, resolveComponent as r, openBlock as x, createBlock as k, withCtx as n, createVNode as l, withModifiers as oe, createCommentVNode as T, createElementVNode as le, createTextVNode as M, toDisplayString as $, createElementBlock as W, Fragment as j, unref as E, renderList as ne, h as se, watch as ue } from "vue";
|
|
2
|
+
import { useTranslate as K, HttpError as re, useAdmin as ie, useI18n as ce } from "@webx-ui/admin";
|
|
3
|
+
const O = /* @__PURE__ */ Symbol("webx-auth");
|
|
4
|
+
function R() {
|
|
5
|
+
const e = ee(O, null);
|
|
6
6
|
if (e === null)
|
|
7
7
|
throw new Error("useAuth() was called outside a panel that installed the auth plugin.");
|
|
8
8
|
return e;
|
|
9
9
|
}
|
|
10
|
-
function
|
|
11
|
-
const
|
|
10
|
+
function de(e) {
|
|
11
|
+
const o = `${e.apiPath}/auth`;
|
|
12
12
|
return {
|
|
13
13
|
async me() {
|
|
14
14
|
try {
|
|
15
|
-
return (await e.http.get(`${
|
|
15
|
+
return (await e.http.get(`${o}/me`)).data;
|
|
16
16
|
} catch (t) {
|
|
17
|
-
if (
|
|
17
|
+
if (me(t))
|
|
18
18
|
return null;
|
|
19
19
|
throw t;
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
async login(t) {
|
|
23
|
-
const
|
|
23
|
+
const a = await e.http.post(`${o}/login`, {
|
|
24
24
|
email: t.email,
|
|
25
25
|
password: t.password,
|
|
26
26
|
remember: t.remember ?? !1
|
|
27
27
|
});
|
|
28
|
-
return e.setUser(
|
|
28
|
+
return e.setUser(a.data), await e.reload(), a.data;
|
|
29
|
+
},
|
|
30
|
+
async setLocale(t) {
|
|
31
|
+
const a = await e.http.put(`${o}/locale`, { locale: t });
|
|
32
|
+
e.setUser(a.data), await e.setLocale(t);
|
|
29
33
|
},
|
|
30
34
|
async logout() {
|
|
31
35
|
try {
|
|
32
|
-
await e.http.post(`${
|
|
36
|
+
await e.http.post(`${o}/logout`);
|
|
33
37
|
} finally {
|
|
34
38
|
e.setUser(null), e.state.status = "unauthenticated", e.state.manifest = null;
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
|
-
function
|
|
40
|
-
e.provide(
|
|
43
|
+
function pe(e, o) {
|
|
44
|
+
e.provide(O, o);
|
|
41
45
|
}
|
|
42
|
-
function
|
|
46
|
+
function me(e) {
|
|
43
47
|
return typeof e == "object" && e !== null && "status" in e && e.status === 401;
|
|
44
48
|
}
|
|
45
|
-
const
|
|
49
|
+
const ve = ["title", "aria-label", "aria-pressed"], fe = /* @__PURE__ */ q({
|
|
46
50
|
__name: "LoginCard",
|
|
47
51
|
props: {
|
|
48
|
-
emailLabel: { default:
|
|
49
|
-
passwordLabel: { default:
|
|
50
|
-
rememberLabel: { default:
|
|
51
|
-
submitLabel: { default:
|
|
52
|
-
capsLockWarning: { default:
|
|
53
|
-
throttleMessage: { default:
|
|
52
|
+
emailLabel: { default: void 0 },
|
|
53
|
+
passwordLabel: { default: void 0 },
|
|
54
|
+
rememberLabel: { default: void 0 },
|
|
55
|
+
submitLabel: { default: void 0 },
|
|
56
|
+
capsLockWarning: { default: void 0 },
|
|
57
|
+
throttleMessage: { default: void 0 },
|
|
58
|
+
revealLabel: { default: void 0 },
|
|
59
|
+
hideLabel: { default: void 0 },
|
|
54
60
|
remember: { type: Boolean, default: !0 },
|
|
55
61
|
autofocus: { type: Boolean, default: !0 }
|
|
56
62
|
},
|
|
57
63
|
emits: ["success"],
|
|
58
|
-
setup(e, { expose:
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
setup(e, { expose: o, emit: t }) {
|
|
65
|
+
const a = e, i = t, d = R(), u = K("webx-auth"), c = C(() => ({
|
|
66
|
+
email: a.emailLabel ?? u("card.email"),
|
|
67
|
+
password: a.passwordLabel ?? u("card.password"),
|
|
68
|
+
remember: a.rememberLabel ?? u("card.remember"),
|
|
69
|
+
submit: a.submitLabel ?? u("card.submit"),
|
|
70
|
+
capsLock: a.capsLockWarning ?? u("card.caps-lock"),
|
|
71
|
+
reveal: a.revealLabel ?? u("card.reveal"),
|
|
72
|
+
hide: a.hideLabel ?? u("card.hide")
|
|
73
|
+
})), V = b(""), v = b(""), p = b(!1), g = b(!1), f = b(!1), S = b(!1), m = b(""), y = b({}), w = b(0), _ = te("emailField");
|
|
74
|
+
let U = null;
|
|
75
|
+
ae(A);
|
|
76
|
+
const z = C(() => w.value > 0), H = C(
|
|
77
|
+
() => a.throttleMessage === void 0 ? u("card.throttled", { seconds: w.value }) : a.throttleMessage.replace(":seconds", String(w.value))
|
|
64
78
|
);
|
|
65
|
-
async function
|
|
66
|
-
if (!(
|
|
67
|
-
|
|
79
|
+
async function D() {
|
|
80
|
+
if (!(g.value || z.value)) {
|
|
81
|
+
g.value = !0, m.value = "", y.value = {};
|
|
68
82
|
try {
|
|
69
83
|
await d.login({
|
|
70
|
-
email:
|
|
84
|
+
email: V.value,
|
|
71
85
|
password: v.value,
|
|
72
|
-
remember:
|
|
86
|
+
remember: p.value
|
|
73
87
|
}), v.value = "", i("success");
|
|
74
|
-
} catch (
|
|
75
|
-
|
|
88
|
+
} catch (s) {
|
|
89
|
+
G(s);
|
|
76
90
|
} finally {
|
|
77
|
-
|
|
91
|
+
g.value = !1;
|
|
78
92
|
}
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
|
-
function
|
|
82
|
-
if (!(
|
|
83
|
-
|
|
95
|
+
function G(s) {
|
|
96
|
+
if (!(s instanceof re)) {
|
|
97
|
+
m.value = s instanceof Error ? s.message : String(s);
|
|
84
98
|
return;
|
|
85
99
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
100
|
+
if (s.isValidation) {
|
|
101
|
+
y.value = s.errors;
|
|
88
102
|
return;
|
|
89
103
|
}
|
|
90
|
-
if (
|
|
91
|
-
|
|
104
|
+
if (s.isThrottled) {
|
|
105
|
+
J(s.retryAfter ?? 60);
|
|
92
106
|
return;
|
|
93
107
|
}
|
|
94
|
-
|
|
108
|
+
m.value = s.message;
|
|
95
109
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
function J(s) {
|
|
111
|
+
A(), w.value = s, U = setInterval(() => {
|
|
112
|
+
w.value -= 1, w.value <= 0 && A();
|
|
99
113
|
}, 1e3);
|
|
100
114
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
115
|
+
function A() {
|
|
116
|
+
U !== null && (clearInterval(U), U = null), w.value = 0;
|
|
103
117
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
118
|
+
function N(s) {
|
|
119
|
+
S.value = s.getModifierState("CapsLock");
|
|
106
120
|
}
|
|
107
|
-
return
|
|
108
|
-
const
|
|
109
|
-
return
|
|
121
|
+
return o({ focus: () => _.value?.focus() }), (s, h) => {
|
|
122
|
+
const I = r("wx-alert"), B = r("wx-icon"), P = r("wx-input"), F = r("wx-form-item"), Q = r("wx-checkbox"), X = r("wx-button"), Y = r("wx-form"), Z = r("wx-card");
|
|
123
|
+
return x(), k(Z, {
|
|
110
124
|
class: "wx-login",
|
|
111
125
|
padding: 24
|
|
112
126
|
}, {
|
|
113
|
-
default:
|
|
114
|
-
|
|
115
|
-
errors:
|
|
116
|
-
disabled:
|
|
127
|
+
default: n(() => [
|
|
128
|
+
l(Y, {
|
|
129
|
+
errors: y.value,
|
|
130
|
+
disabled: g.value,
|
|
117
131
|
gap: "md",
|
|
118
|
-
onSubmit:
|
|
132
|
+
onSubmit: oe(D, ["prevent"])
|
|
119
133
|
}, {
|
|
120
|
-
default:
|
|
121
|
-
|
|
134
|
+
default: n(() => [
|
|
135
|
+
m.value !== "" ? (x(), k(I, {
|
|
122
136
|
key: 0,
|
|
123
137
|
type: "danger",
|
|
124
138
|
variant: "soft",
|
|
125
139
|
live: "",
|
|
126
|
-
description:
|
|
140
|
+
description: m.value,
|
|
127
141
|
class: "wx-login__message"
|
|
128
|
-
}, null, 8, ["description"])) :
|
|
142
|
+
}, null, 8, ["description"])) : z.value ? (x(), k(I, {
|
|
129
143
|
key: 1,
|
|
130
144
|
type: "warning",
|
|
131
145
|
variant: "soft",
|
|
132
146
|
live: "",
|
|
133
|
-
description:
|
|
147
|
+
description: H.value,
|
|
134
148
|
class: "wx-login__message"
|
|
135
|
-
}, null, 8, ["description"])) :
|
|
136
|
-
|
|
137
|
-
default:
|
|
138
|
-
|
|
149
|
+
}, null, 8, ["description"])) : T("", !0),
|
|
150
|
+
l(F, { name: "email" }, {
|
|
151
|
+
default: n(() => [
|
|
152
|
+
l(P, {
|
|
139
153
|
ref_key: "emailField",
|
|
140
|
-
ref:
|
|
141
|
-
modelValue:
|
|
142
|
-
"onUpdate:modelValue":
|
|
154
|
+
ref: _,
|
|
155
|
+
modelValue: V.value,
|
|
156
|
+
"onUpdate:modelValue": h[0] || (h[0] = (L) => V.value = L),
|
|
143
157
|
type: "email",
|
|
144
|
-
placeholder:
|
|
145
|
-
"aria-label":
|
|
158
|
+
placeholder: c.value.email,
|
|
159
|
+
"aria-label": c.value.email,
|
|
146
160
|
autocomplete: "username",
|
|
147
161
|
autofocus: e.autofocus,
|
|
148
162
|
inputmode: "email",
|
|
149
163
|
size: "lg"
|
|
150
164
|
}, {
|
|
151
|
-
prefix:
|
|
152
|
-
|
|
165
|
+
prefix: n(() => [
|
|
166
|
+
l(B, { name: "mail" })
|
|
153
167
|
]),
|
|
154
168
|
_: 1
|
|
155
169
|
}, 8, ["modelValue", "placeholder", "aria-label", "autofocus"])
|
|
156
170
|
]),
|
|
157
171
|
_: 1
|
|
158
172
|
}),
|
|
159
|
-
|
|
173
|
+
l(F, {
|
|
160
174
|
name: "password",
|
|
161
|
-
help:
|
|
175
|
+
help: S.value ? c.value.capsLock : void 0
|
|
162
176
|
}, {
|
|
163
|
-
default:
|
|
164
|
-
|
|
177
|
+
default: n(() => [
|
|
178
|
+
l(P, {
|
|
165
179
|
modelValue: v.value,
|
|
166
|
-
"onUpdate:modelValue":
|
|
167
|
-
type:
|
|
168
|
-
placeholder:
|
|
169
|
-
"aria-label":
|
|
180
|
+
"onUpdate:modelValue": h[2] || (h[2] = (L) => v.value = L),
|
|
181
|
+
type: f.value ? "text" : "password",
|
|
182
|
+
placeholder: c.value.password,
|
|
183
|
+
"aria-label": c.value.password,
|
|
170
184
|
autocomplete: "current-password",
|
|
171
185
|
size: "lg",
|
|
172
|
-
onKeydown:
|
|
173
|
-
onKeyup:
|
|
186
|
+
onKeydown: N,
|
|
187
|
+
onKeyup: N
|
|
174
188
|
}, {
|
|
175
|
-
prefix:
|
|
176
|
-
|
|
189
|
+
prefix: n(() => [
|
|
190
|
+
l(B, { name: "lock" })
|
|
177
191
|
]),
|
|
178
|
-
suffix:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
suffix: n(() => [
|
|
193
|
+
le("button", {
|
|
194
|
+
type: "button",
|
|
195
|
+
class: "wx-login__reveal",
|
|
196
|
+
title: f.value ? c.value.hide : c.value.reveal,
|
|
197
|
+
"aria-label": f.value ? c.value.hide : c.value.reveal,
|
|
198
|
+
"aria-pressed": f.value,
|
|
182
199
|
tabindex: "-1",
|
|
183
|
-
onClick:
|
|
184
|
-
},
|
|
200
|
+
onClick: h[1] || (h[1] = (L) => f.value = !f.value)
|
|
201
|
+
}, [
|
|
202
|
+
l(B, {
|
|
203
|
+
name: f.value ? "eye-off" : "eye"
|
|
204
|
+
}, null, 8, ["name"])
|
|
205
|
+
], 8, ve)
|
|
185
206
|
]),
|
|
186
207
|
_: 1
|
|
187
208
|
}, 8, ["modelValue", "type", "placeholder", "aria-label"])
|
|
188
209
|
]),
|
|
189
210
|
_: 1
|
|
190
211
|
}, 8, ["help"]),
|
|
191
|
-
e.remember ? (
|
|
212
|
+
e.remember ? (x(), k(Q, {
|
|
192
213
|
key: 2,
|
|
193
|
-
modelValue:
|
|
194
|
-
"onUpdate:modelValue":
|
|
195
|
-
label:
|
|
196
|
-
}, null, 8, ["modelValue", "label"])) :
|
|
197
|
-
|
|
214
|
+
modelValue: p.value,
|
|
215
|
+
"onUpdate:modelValue": h[3] || (h[3] = (L) => p.value = L),
|
|
216
|
+
label: c.value.remember
|
|
217
|
+
}, null, 8, ["modelValue", "label"])) : T("", !0),
|
|
218
|
+
l(X, {
|
|
198
219
|
type: "primary",
|
|
199
220
|
"native-type": "submit",
|
|
200
221
|
size: "lg",
|
|
201
222
|
block: "",
|
|
202
|
-
loading:
|
|
203
|
-
disabled:
|
|
223
|
+
loading: g.value,
|
|
224
|
+
disabled: z.value
|
|
204
225
|
}, {
|
|
205
|
-
default:
|
|
206
|
-
M($(
|
|
226
|
+
default: n(() => [
|
|
227
|
+
M($(c.value.submit), 1)
|
|
207
228
|
]),
|
|
208
229
|
_: 1
|
|
209
230
|
}, 8, ["loading", "disabled"])
|
|
@@ -215,82 +236,131 @@ const ue = /* @__PURE__ */ N({
|
|
|
215
236
|
});
|
|
216
237
|
};
|
|
217
238
|
}
|
|
218
|
-
}),
|
|
239
|
+
}), we = (e, o) => {
|
|
219
240
|
const t = e.__vccOpts || e;
|
|
220
|
-
for (const [
|
|
221
|
-
t[
|
|
241
|
+
for (const [a, i] of o)
|
|
242
|
+
t[a] = i;
|
|
222
243
|
return t;
|
|
223
|
-
},
|
|
244
|
+
}, be = /* @__PURE__ */ we(fe, [["__scopeId", "data-v-1224c1f8"]]), _e = {
|
|
245
|
+
card: {
|
|
246
|
+
email: "Email",
|
|
247
|
+
password: "Password",
|
|
248
|
+
remember: "Stay signed in",
|
|
249
|
+
submit: "Sign in",
|
|
250
|
+
"caps-lock": "Caps Lock is on.",
|
|
251
|
+
reveal: "Show the password",
|
|
252
|
+
hide: "Hide the password",
|
|
253
|
+
throttled: "Too many attempts. Try again in :seconds s.",
|
|
254
|
+
failed: "Those details do not match an account."
|
|
255
|
+
},
|
|
256
|
+
menu: {
|
|
257
|
+
"sign-out": "Sign out",
|
|
258
|
+
language: "Interface language"
|
|
259
|
+
}
|
|
260
|
+
}, ge = /* @__PURE__ */ q({
|
|
224
261
|
__name: "UserMenu",
|
|
225
262
|
props: {
|
|
226
|
-
signOutLabel: { default:
|
|
263
|
+
signOutLabel: { default: void 0 }
|
|
227
264
|
},
|
|
228
265
|
setup(e) {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
266
|
+
const o = ie(), t = R(), a = ce(), i = K("webx-auth"), d = C(() => o.state.user), u = C(
|
|
267
|
+
() => a.state.panelLocales.length > 1 ? a.state.panelLocales : []
|
|
268
|
+
), c = C(() => (d.value?.name ?? "").split(/\s+/).filter((p) => p !== "").slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join(""));
|
|
269
|
+
async function V(v) {
|
|
270
|
+
v !== a.state.locale && await t.setLocale(v);
|
|
271
|
+
}
|
|
272
|
+
return (v, p) => {
|
|
273
|
+
const g = r("wx-avatar"), f = r("wx-action"), S = r("wx-text"), m = r("wx-dropdown-item"), y = r("wx-divider"), w = r("wx-dropdown");
|
|
274
|
+
return d.value !== null ? (x(), k(w, { key: 0 }, {
|
|
275
|
+
trigger: n(() => [
|
|
276
|
+
l(f, {
|
|
277
|
+
title: d.value.name
|
|
236
278
|
}, {
|
|
237
|
-
default:
|
|
238
|
-
|
|
239
|
-
label:
|
|
279
|
+
default: n(() => [
|
|
280
|
+
l(g, {
|
|
281
|
+
label: c.value,
|
|
240
282
|
size: "sm"
|
|
241
283
|
}, null, 8, ["label"])
|
|
242
284
|
]),
|
|
243
285
|
_: 1
|
|
244
286
|
}, 8, ["title"])
|
|
245
287
|
]),
|
|
246
|
-
default:
|
|
247
|
-
|
|
248
|
-
default:
|
|
249
|
-
|
|
250
|
-
default:
|
|
251
|
-
M($(
|
|
288
|
+
default: n(() => [
|
|
289
|
+
l(m, { disabled: "" }, {
|
|
290
|
+
default: n(() => [
|
|
291
|
+
l(S, { size: "sm" }, {
|
|
292
|
+
default: n(() => [
|
|
293
|
+
M($(d.value.email), 1)
|
|
252
294
|
]),
|
|
253
295
|
_: 1
|
|
254
296
|
})
|
|
255
297
|
]),
|
|
256
298
|
_: 1
|
|
257
299
|
}),
|
|
258
|
-
|
|
300
|
+
u.value.length > 0 ? (x(), W(j, { key: 0 }, [
|
|
301
|
+
l(y, { spacing: 4 }),
|
|
302
|
+
l(m, { disabled: "" }, {
|
|
303
|
+
default: n(() => [
|
|
304
|
+
l(S, {
|
|
305
|
+
size: "sm",
|
|
306
|
+
tone: "muted"
|
|
307
|
+
}, {
|
|
308
|
+
default: n(() => [
|
|
309
|
+
M($(E(i)("menu.language")), 1)
|
|
310
|
+
]),
|
|
311
|
+
_: 1
|
|
312
|
+
})
|
|
313
|
+
]),
|
|
314
|
+
_: 1
|
|
315
|
+
}),
|
|
316
|
+
(x(!0), W(j, null, ne(u.value, (_) => (x(), k(m, {
|
|
317
|
+
key: _.code,
|
|
318
|
+
icon: _.code === E(a).state.locale ? "check" : void 0,
|
|
319
|
+
onClick: (U) => V(_.code)
|
|
320
|
+
}, {
|
|
321
|
+
default: n(() => [
|
|
322
|
+
M($(_.nativeName), 1)
|
|
323
|
+
]),
|
|
324
|
+
_: 2
|
|
325
|
+
}, 1032, ["icon", "onClick"]))), 128)),
|
|
326
|
+
l(y, { spacing: 4 })
|
|
327
|
+
], 64)) : T("", !0),
|
|
328
|
+
l(m, {
|
|
259
329
|
icon: "logout",
|
|
260
|
-
onClick:
|
|
330
|
+
onClick: p[0] || (p[0] = (_) => E(t).logout())
|
|
261
331
|
}, {
|
|
262
|
-
default:
|
|
263
|
-
M($(e.signOutLabel), 1)
|
|
332
|
+
default: n(() => [
|
|
333
|
+
M($(e.signOutLabel ?? E(i)("menu.sign-out")), 1)
|
|
264
334
|
]),
|
|
265
335
|
_: 1
|
|
266
336
|
})
|
|
267
337
|
]),
|
|
268
338
|
_: 1
|
|
269
|
-
})) :
|
|
339
|
+
})) : T("", !0);
|
|
270
340
|
};
|
|
271
341
|
}
|
|
272
342
|
});
|
|
273
|
-
function
|
|
274
|
-
const
|
|
343
|
+
function ye(e = {}) {
|
|
344
|
+
const o = e.path ?? "/login";
|
|
275
345
|
return {
|
|
276
346
|
install(t) {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
path:
|
|
347
|
+
const a = de(t.context);
|
|
348
|
+
t.i18n.defaults("webx-auth", _e), pe(t.app, a), t.context.useSessionLoader(() => a.me()), t.router.addRoute({
|
|
349
|
+
path: o,
|
|
280
350
|
name: "webx.login",
|
|
281
351
|
meta: { public: !0 },
|
|
282
|
-
component: { render: () =>
|
|
283
|
-
}), t.router.beforeEach((i) => i.meta.public === !0 || t.context.state.status !== "unauthenticated" ? !0 : { path:
|
|
352
|
+
component: { render: () => se(be, { ...e.card }) }
|
|
353
|
+
}), t.router.beforeEach((i) => i.meta.public === !0 || t.context.state.status !== "unauthenticated" ? !0 : { path: o, query: i.fullPath === "/" ? {} : { next: i.fullPath } }), ue(
|
|
284
354
|
() => t.context.state.status,
|
|
285
355
|
(i) => {
|
|
286
356
|
const d = t.router.currentRoute.value;
|
|
287
|
-
if (i === "unauthenticated" && d.path !==
|
|
288
|
-
t.router.replace({ path:
|
|
357
|
+
if (i === "unauthenticated" && d.path !== o) {
|
|
358
|
+
t.router.replace({ path: o, query: { next: d.fullPath } });
|
|
289
359
|
return;
|
|
290
360
|
}
|
|
291
|
-
if (i === "ready" && d.path ===
|
|
292
|
-
const
|
|
293
|
-
t.router.replace(typeof
|
|
361
|
+
if (i === "ready" && d.path === o) {
|
|
362
|
+
const u = d.query.next;
|
|
363
|
+
t.router.replace(typeof u == "string" ? u : "/");
|
|
294
364
|
}
|
|
295
365
|
}
|
|
296
366
|
);
|
|
@@ -298,12 +368,13 @@ function pe(e = {}) {
|
|
|
298
368
|
};
|
|
299
369
|
}
|
|
300
370
|
export {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
371
|
+
be as WxLoginCard,
|
|
372
|
+
ge as WxUserMenu,
|
|
373
|
+
ye as auth,
|
|
374
|
+
O as authKey,
|
|
375
|
+
_e as authMessages,
|
|
376
|
+
de as createAuthSession,
|
|
377
|
+
pe as provideAuth,
|
|
378
|
+
R as useAuth
|
|
308
379
|
};
|
|
309
380
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/session.ts","../src/LoginCard.vue","../src/UserMenu.vue","../src/index.ts"],"sourcesContent":["import { inject, type App, type InjectionKey } from 'vue'\nimport type { AdminContext, AdminUser } from '@webx-ui/admin'\n\nexport interface Credentials {\n email: string\n password: string\n remember?: boolean\n}\n\nexport interface AuthSession {\n /** Who is signed in, or null. A 401 is an answer, not a failure. */\n me(): Promise<AdminUser | null>\n /** Signs in and tells the panel to load itself. Throws `HttpError` on a refusal. */\n login(credentials: Credentials): Promise<AdminUser>\n logout(): Promise<void>\n}\n\nexport const authKey: InjectionKey<AuthSession> = Symbol('webx-auth')\n\nexport function useAuth(): AuthSession {\n const session = inject(authKey, null)\n\n if (session === null) {\n throw new Error('useAuth() was called outside a panel that installed the auth plugin.')\n }\n\n return session\n}\n\n/**\n * The three calls `webx-ui/module-auth` answers, and nothing else.\n *\n * The session lives in a cookie, so there is nothing to keep here: the source of truth is the\n * server, and the panel asks it rather than remembering.\n */\nexport function createAuthSession(admin: AdminContext): AuthSession {\n const base = `${admin.apiPath}/auth`\n\n return {\n async me() {\n try {\n const body = await admin.http.get<{ data: AdminUser | null }>(`${base}/me`)\n\n return body.data\n } catch (error) {\n if (isUnauthenticated(error)) {\n return null\n }\n\n throw error\n }\n },\n\n async login(credentials) {\n const body = await admin.http.post<{ data: AdminUser }>(`${base}/login`, {\n email: credentials.email,\n password: credentials.password,\n remember: credentials.remember ?? false,\n })\n\n admin.setUser(body.data)\n\n // Signing in is the moment the panel becomes knowable: the manifest needs a session,\n // so it could not have been loaded before now.\n await admin.reload()\n\n return body.data\n },\n\n async logout() {\n try {\n await admin.http.post(`${base}/logout`)\n } finally {\n // Whatever the server said, this browser is done: leaving the panel drawn behind a\n // failed sign-out would be worse than signing out optimistically.\n admin.setUser(null)\n admin.state.status = 'unauthenticated'\n admin.state.manifest = null\n }\n },\n }\n}\n\nexport function provideAuth(app: App, session: AuthSession): void {\n app.provide(authKey, session)\n}\n\nfunction isUnauthenticated(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status: unknown }).status === 401\n )\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'\nimport { HttpError } from '@webx-ui/admin'\nimport { useAuth } from './session'\n\n/**\n * The card a panel opens on.\n *\n * No logo, no product name, no \"welcome back\" — an admin panel's front door should say as\n * little as possible to somebody who has no business behind it, and the people who do have\n * business behind it already know where they are.\n *\n * Every string is a prop. The library speaks English; the panels built with it do not have to.\n */\nconst props = withDefaults(\n defineProps<{\n emailLabel?: string\n passwordLabel?: string\n rememberLabel?: string\n submitLabel?: string\n capsLockWarning?: string\n throttleMessage?: string\n /** Offer to stay signed in. The server honours it with a long-lived cookie. */\n remember?: boolean\n /** Put the cursor in the email field. Off for a card that is not the only thing on screen. */\n autofocus?: boolean\n }>(),\n {\n emailLabel: 'Email',\n passwordLabel: 'Password',\n rememberLabel: 'Stay signed in',\n submitLabel: 'Sign in',\n capsLockWarning: 'Caps Lock is on.',\n // `{seconds}` is replaced with however long the server said to wait.\n throttleMessage: 'Too many attempts. Try again in {seconds} s.',\n remember: true,\n autofocus: true,\n },\n)\n\nconst emit = defineEmits<{ success: [] }>()\n\nconst auth = useAuth()\n\nconst email = ref('')\nconst password = ref('')\nconst rememberMe = ref(false)\n\nconst busy = ref(false)\nconst revealed = ref(false)\nconst capsLock = ref(false)\nconst message = ref('')\nconst errors = ref<Record<string, string[]>>({})\nconst secondsLeft = ref(0)\n\nconst emailField = useTemplateRef<HTMLInputElement>('emailField')\n\nlet countdown: ReturnType<typeof setInterval> | null = null\n\nonBeforeUnmount(stopCountdown)\n\nconst throttled = computed(() => secondsLeft.value > 0)\n\nconst throttleNotice = computed(() =>\n props.throttleMessage.replace('{seconds}', String(secondsLeft.value)),\n)\n\nasync function submit(): Promise<void> {\n if (busy.value || throttled.value) {\n return\n }\n\n busy.value = true\n message.value = ''\n errors.value = {}\n\n try {\n await auth.login({\n email: email.value,\n password: password.value,\n remember: rememberMe.value,\n })\n\n password.value = ''\n emit('success')\n } catch (error) {\n handle(error)\n } finally {\n busy.value = false\n }\n}\n\nfunction handle(error: unknown): void {\n if (!(error instanceof HttpError)) {\n message.value = error instanceof Error ? error.message : String(error)\n\n return\n }\n\n // A bad password comes back as a 422 against the email field, the same as an address\n // nobody owns — the server refuses to say which, and so does this.\n if (error.isValidation) {\n errors.value = error.errors\n\n return\n }\n\n if (error.isThrottled) {\n startCountdown(error.retryAfter ?? 60)\n\n return\n }\n\n message.value = error.message\n}\n\nfunction startCountdown(seconds: number): void {\n stopCountdown()\n secondsLeft.value = seconds\n\n countdown = setInterval(() => {\n secondsLeft.value -= 1\n\n if (secondsLeft.value <= 0) {\n stopCountdown()\n }\n }, 1000)\n}\n\nfunction stopCountdown(): void {\n if (countdown !== null) {\n clearInterval(countdown)\n countdown = null\n }\n\n secondsLeft.value = 0\n}\n\n/**\n * Worth saying out loud: a password typed in capitals fails with the same unhelpful message\n * as a wrong one, and people retype it three times before noticing.\n */\nfunction trackCapsLock(event: KeyboardEvent): void {\n capsLock.value = event.getModifierState('CapsLock')\n}\n\ndefineExpose({ focus: () => emailField.value?.focus() })\n</script>\n\n<template>\n <wx-card class=\"wx-login\" :padding=\"24\">\n <wx-form :errors=\"errors\" :disabled=\"busy\" gap=\"md\" @submit.prevent=\"submit\">\n <wx-alert\n v-if=\"message !== ''\"\n type=\"danger\"\n variant=\"soft\"\n live\n :description=\"message\"\n class=\"wx-login__message\"\n />\n\n <wx-alert\n v-else-if=\"throttled\"\n type=\"warning\"\n variant=\"soft\"\n live\n :description=\"throttleNotice\"\n class=\"wx-login__message\"\n />\n\n <wx-form-item name=\"email\">\n <wx-input\n ref=\"emailField\"\n v-model=\"email\"\n type=\"email\"\n :placeholder=\"emailLabel\"\n :aria-label=\"emailLabel\"\n autocomplete=\"username\"\n :autofocus=\"autofocus\"\n inputmode=\"email\"\n size=\"lg\"\n >\n <template #prefix><wx-icon name=\"mail\" /></template>\n </wx-input>\n </wx-form-item>\n\n <wx-form-item name=\"password\" :help=\"capsLock ? capsLockWarning : undefined\">\n <wx-input\n v-model=\"password\"\n :type=\"revealed ? 'text' : 'password'\"\n :placeholder=\"passwordLabel\"\n :aria-label=\"passwordLabel\"\n autocomplete=\"current-password\"\n size=\"lg\"\n @keydown=\"trackCapsLock\"\n @keyup=\"trackCapsLock\"\n >\n <template #prefix><wx-icon name=\"lock\" /></template>\n <template #suffix>\n <wx-action\n :icon=\"revealed ? 'eye-off' : 'eye'\"\n :title=\"revealed ? 'Hide the password' : 'Show the password'\"\n tabindex=\"-1\"\n @click=\"revealed = !revealed\"\n />\n </template>\n </wx-input>\n </wx-form-item>\n\n <wx-checkbox v-if=\"remember\" v-model=\"rememberMe\" :label=\"rememberLabel\" />\n\n <wx-button\n type=\"primary\"\n native-type=\"submit\"\n size=\"lg\"\n block\n :loading=\"busy\"\n :disabled=\"throttled\"\n >\n {{ submitLabel }}\n </wx-button>\n </wx-form>\n </wx-card>\n</template>\n\n<style scoped>\n.wx-login {\n width: 100%;\n /* Wide enough for an email address, narrow enough to stay a card rather than a page. */\n max-width: 26rem;\n}\n\n.wx-login__message {\n margin-block-end: var(--wx-space-4);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useAdmin } from '@webx-ui/admin'\nimport { useAuth } from './session'\n\n/**\n * The corner of the header: who this is, and the way out.\n */\nwithDefaults(defineProps<{ signOutLabel?: string }>(), { signOutLabel: 'Sign out' })\n\nconst admin = useAdmin()\nconst auth = useAuth()\n\nconst user = computed(() => admin.state.user)\n\nconst initials = computed(() => {\n const name = user.value?.name ?? ''\n\n return name\n .split(/\\s+/)\n .filter((part) => part !== '')\n .slice(0, 2)\n .map((part) => part[0]?.toUpperCase() ?? '')\n .join('')\n})\n</script>\n\n<template>\n <wx-dropdown v-if=\"user !== null\">\n <template #trigger>\n <wx-action :title=\"user.name\">\n <wx-avatar :label=\"initials\" size=\"sm\" />\n </wx-action>\n </template>\n\n <wx-dropdown-item disabled>\n <wx-text size=\"sm\">{{ user.email }}</wx-text>\n </wx-dropdown-item>\n\n <wx-dropdown-item icon=\"logout\" @click=\"auth.logout()\">{{ signOutLabel }}</wx-dropdown-item>\n </wx-dropdown>\n</template>\n","import { h, watch } from 'vue'\nimport type { Admin, AdminPlugin } from '@webx-ui/admin'\nimport LoginCard from './LoginCard.vue'\nimport { createAuthSession, provideAuth } from './session'\n\nexport interface AuthOptions {\n /** Where the sign-in screen lives, inside the panel. */\n path?: string\n /** Props handed to the card — the labels, mostly. */\n card?: Record<string, unknown>\n}\n\n/**\n * Signing in, as a plugin rather than a section: it owns a route and a guard, and it has no\n * place in the menu.\n *\n * Installing it also tells the panel how to find out who is signed in, which is why the panel\n * itself knows nothing about authentication — it holds the answer, not the question.\n */\nexport function auth(options: AuthOptions = {}): AdminPlugin {\n const path = options.path ?? '/login'\n\n return {\n install(admin: Admin) {\n const session = createAuthSession(admin.context)\n\n provideAuth(admin.app, session)\n admin.context.useSessionLoader(() => session.me())\n\n admin.router.addRoute({\n path,\n name: 'webx.login',\n meta: { public: true },\n component: { render: () => h(LoginCard, { ...options.card }) },\n })\n\n admin.router.beforeEach((to) => {\n if (to.meta.public === true) {\n return true\n }\n\n if (admin.context.state.status !== 'unauthenticated') {\n return true\n }\n\n return { path, query: to.fullPath === '/' ? {} : { next: to.fullPath } }\n })\n\n // Both directions are driven by the state rather than by an event, because the card\n // cannot be relied on to announce its own success: signing in flips the panel to\n // `ready`, the shell swaps the branch it renders, and the card is unmounted before the\n // line after `await` runs.\n watch(\n () => admin.context.state.status,\n (status) => {\n const current = admin.router.currentRoute.value\n\n // A session can also end between requests — a sign-out in another tab, an account\n // switched off. The panel learns that from a 401 on whatever it asked for next,\n // which is not a navigation, so nothing would move without this.\n if (status === 'unauthenticated' && current.path !== path) {\n void admin.router.replace({ path, query: { next: current.fullPath } })\n\n return\n }\n\n if (status === 'ready' && current.path === path) {\n const next = current.query.next\n\n void admin.router.replace(typeof next === 'string' ? next : '/')\n }\n },\n )\n },\n }\n}\n\nexport { createAuthSession, provideAuth, useAuth, authKey } from './session'\nexport type { AuthSession, Credentials } from './session'\nexport { default as WxLoginCard } from './LoginCard.vue'\nexport { default as WxUserMenu } from './UserMenu.vue'\n"],"names":["authKey","useAuth","session","inject","createAuthSession","admin","base","error","isUnauthenticated","credentials","body","provideAuth","app","props","__props","emit","__emit","auth","email","ref","password","rememberMe","busy","revealed","capsLock","message","errors","secondsLeft","emailField","useTemplateRef","countdown","onBeforeUnmount","stopCountdown","throttled","computed","throttleNotice","submit","handle","HttpError","startCountdown","seconds","trackCapsLock","event","__expose","_createBlock","_component_wx_card","_createVNode","_component_wx_form","_component_wx_alert","_component_wx_form_item","_component_wx_input","$event","_component_wx_icon","_component_wx_action","_cache","_component_wx_checkbox","_component_wx_button","useAdmin","user","initials","part","_component_wx_dropdown","_component_wx_avatar","_component_wx_dropdown_item","_component_wx_text","_createTextVNode","_toDisplayString","_unref","options","path","h","LoginCard","to","watch","status","current","next"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/session.ts","../src/LoginCard.vue","../src/messages.ts","../src/UserMenu.vue","../src/index.ts"],"sourcesContent":["import { inject, type App, type InjectionKey } from 'vue'\nimport type { AdminContext, AdminUser } from '@webx-ui/admin'\n\nexport interface Credentials {\n email: string\n password: string\n remember?: boolean\n}\n\nexport interface AuthSession {\n /** Who is signed in, or null. A 401 is an answer, not a failure. */\n me(): Promise<AdminUser | null>\n /** Signs in and tells the panel to load itself. Throws `HttpError` on a refusal. */\n login(credentials: Credentials): Promise<AdminUser>\n logout(): Promise<void>\n /**\n * Change the language this administrator reads the panel in.\n *\n * Stored on the person rather than in this browser, so it follows them to the next machine\n * and so the server writes its own messages — a 422 under a field — in the same language as\n * the label above it.\n */\n setLocale(code: string): Promise<void>\n}\n\nexport const authKey: InjectionKey<AuthSession> = Symbol('webx-auth')\n\nexport function useAuth(): AuthSession {\n const session = inject(authKey, null)\n\n if (session === null) {\n throw new Error('useAuth() was called outside a panel that installed the auth plugin.')\n }\n\n return session\n}\n\n/**\n * The three calls `webx-ui/module-auth` answers, and nothing else.\n *\n * The session lives in a cookie, so there is nothing to keep here: the source of truth is the\n * server, and the panel asks it rather than remembering.\n */\nexport function createAuthSession(admin: AdminContext): AuthSession {\n const base = `${admin.apiPath}/auth`\n\n return {\n async me() {\n try {\n const body = await admin.http.get<{ data: AdminUser | null }>(`${base}/me`)\n\n return body.data\n } catch (error) {\n if (isUnauthenticated(error)) {\n return null\n }\n\n throw error\n }\n },\n\n async login(credentials) {\n const body = await admin.http.post<{ data: AdminUser }>(`${base}/login`, {\n email: credentials.email,\n password: credentials.password,\n remember: credentials.remember ?? false,\n })\n\n admin.setUser(body.data)\n\n // Signing in is the moment the panel becomes knowable: the manifest needs a session,\n // so it could not have been loaded before now.\n await admin.reload()\n\n return body.data\n },\n\n async setLocale(code) {\n // The panel redraws only after the server has accepted the choice: a language that\n // half took — menus switched, error messages not — is worse than one that did not.\n const body = await admin.http.put<{ data: AdminUser }>(`${base}/locale`, { locale: code })\n\n admin.setUser(body.data)\n\n await admin.setLocale(code)\n },\n\n async logout() {\n try {\n await admin.http.post(`${base}/logout`)\n } finally {\n // Whatever the server said, this browser is done: leaving the panel drawn behind a\n // failed sign-out would be worse than signing out optimistically.\n admin.setUser(null)\n admin.state.status = 'unauthenticated'\n admin.state.manifest = null\n }\n },\n }\n}\n\nexport function provideAuth(app: App, session: AuthSession): void {\n app.provide(authKey, session)\n}\n\nfunction isUnauthenticated(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status: unknown }).status === 401\n )\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'\nimport { HttpError, useTranslate } from '@webx-ui/admin'\nimport { useAuth } from './session'\n\n/**\n * The card a panel opens on.\n *\n * No logo, no product name, no \"welcome back\" — an admin panel's front door should say as\n * little as possible to somebody who has no business behind it, and the people who do have\n * business behind it already know where they are.\n *\n * Every string is still a prop and a prop given still wins. What changed is what happens when\n * one is not: the label comes from the panel's dictionary, which the server assembles from the\n * same `lang` files it answers its own messages from. A panel gets its language without being\n * told every label twice, and a card placed outside a panel still speaks English.\n */\nconst props = withDefaults(\n defineProps<{\n emailLabel?: string\n passwordLabel?: string\n rememberLabel?: string\n submitLabel?: string\n capsLockWarning?: string\n /** `:seconds` is replaced with however long the server said to wait. */\n throttleMessage?: string\n revealLabel?: string\n hideLabel?: string\n /** Offer to stay signed in. The server honours it with a long-lived cookie. */\n remember?: boolean\n /** Put the cursor in the email field. Off for a card that is not the only thing on screen. */\n autofocus?: boolean\n }>(),\n {\n // Undefined rather than a string, so \"not given\" is tellable from \"given as English\" and\n // the dictionary gets its turn.\n emailLabel: undefined,\n passwordLabel: undefined,\n rememberLabel: undefined,\n submitLabel: undefined,\n capsLockWarning: undefined,\n throttleMessage: undefined,\n revealLabel: undefined,\n hideLabel: undefined,\n remember: true,\n autofocus: true,\n },\n)\n\nconst emit = defineEmits<{ success: [] }>()\n\nconst auth = useAuth()\nconst t = useTranslate('webx-auth')\n\nconst labels = computed(() => ({\n email: props.emailLabel ?? t('card.email'),\n password: props.passwordLabel ?? t('card.password'),\n remember: props.rememberLabel ?? t('card.remember'),\n submit: props.submitLabel ?? t('card.submit'),\n capsLock: props.capsLockWarning ?? t('card.caps-lock'),\n reveal: props.revealLabel ?? t('card.reveal'),\n hide: props.hideLabel ?? t('card.hide'),\n}))\n\nconst email = ref('')\nconst password = ref('')\nconst rememberMe = ref(false)\n\nconst busy = ref(false)\nconst revealed = ref(false)\nconst capsLock = ref(false)\nconst message = ref('')\nconst errors = ref<Record<string, string[]>>({})\nconst secondsLeft = ref(0)\n\nconst emailField = useTemplateRef<HTMLInputElement>('emailField')\n\nlet countdown: ReturnType<typeof setInterval> | null = null\n\nonBeforeUnmount(stopCountdown)\n\nconst throttled = computed(() => secondsLeft.value > 0)\n\nconst throttleNotice = computed(() =>\n props.throttleMessage === undefined\n ? t('card.throttled', { seconds: secondsLeft.value })\n : props.throttleMessage.replace(':seconds', String(secondsLeft.value)),\n)\n\nasync function submit(): Promise<void> {\n if (busy.value || throttled.value) {\n return\n }\n\n busy.value = true\n message.value = ''\n errors.value = {}\n\n try {\n await auth.login({\n email: email.value,\n password: password.value,\n remember: rememberMe.value,\n })\n\n password.value = ''\n emit('success')\n } catch (error) {\n handle(error)\n } finally {\n busy.value = false\n }\n}\n\nfunction handle(error: unknown): void {\n if (!(error instanceof HttpError)) {\n message.value = error instanceof Error ? error.message : String(error)\n\n return\n }\n\n // A bad password comes back as a 422 against the email field, the same as an address\n // nobody owns — the server refuses to say which, and so does this.\n if (error.isValidation) {\n errors.value = error.errors\n\n return\n }\n\n if (error.isThrottled) {\n startCountdown(error.retryAfter ?? 60)\n\n return\n }\n\n message.value = error.message\n}\n\nfunction startCountdown(seconds: number): void {\n stopCountdown()\n secondsLeft.value = seconds\n\n countdown = setInterval(() => {\n secondsLeft.value -= 1\n\n if (secondsLeft.value <= 0) {\n stopCountdown()\n }\n }, 1000)\n}\n\nfunction stopCountdown(): void {\n if (countdown !== null) {\n clearInterval(countdown)\n countdown = null\n }\n\n secondsLeft.value = 0\n}\n\n/**\n * Worth saying out loud: a password typed in capitals fails with the same unhelpful message\n * as a wrong one, and people retype it three times before noticing.\n */\nfunction trackCapsLock(event: KeyboardEvent): void {\n capsLock.value = event.getModifierState('CapsLock')\n}\n\ndefineExpose({ focus: () => emailField.value?.focus() })\n</script>\n\n<template>\n <wx-card class=\"wx-login\" :padding=\"24\">\n <wx-form :errors=\"errors\" :disabled=\"busy\" gap=\"md\" @submit.prevent=\"submit\">\n <wx-alert\n v-if=\"message !== ''\"\n type=\"danger\"\n variant=\"soft\"\n live\n :description=\"message\"\n class=\"wx-login__message\"\n />\n\n <wx-alert\n v-else-if=\"throttled\"\n type=\"warning\"\n variant=\"soft\"\n live\n :description=\"throttleNotice\"\n class=\"wx-login__message\"\n />\n\n <wx-form-item name=\"email\">\n <wx-input\n ref=\"emailField\"\n v-model=\"email\"\n type=\"email\"\n :placeholder=\"labels.email\"\n :aria-label=\"labels.email\"\n autocomplete=\"username\"\n :autofocus=\"autofocus\"\n inputmode=\"email\"\n size=\"lg\"\n >\n <template #prefix><wx-icon name=\"mail\" /></template>\n </wx-input>\n </wx-form-item>\n\n <wx-form-item name=\"password\" :help=\"capsLock ? labels.capsLock : undefined\">\n <wx-input\n v-model=\"password\"\n :type=\"revealed ? 'text' : 'password'\"\n :placeholder=\"labels.password\"\n :aria-label=\"labels.password\"\n autocomplete=\"current-password\"\n size=\"lg\"\n @keydown=\"trackCapsLock\"\n @keyup=\"trackCapsLock\"\n >\n <template #prefix><wx-icon name=\"lock\" /></template>\n <template #suffix>\n <!-- A bare icon rather than a button with a surface: it sits inside the field, and\n a second box in there reads as a second control. Out of the tab order because\n it is a convenience, not a step. -->\n <button\n type=\"button\"\n class=\"wx-login__reveal\"\n :title=\"revealed ? labels.hide : labels.reveal\"\n :aria-label=\"revealed ? labels.hide : labels.reveal\"\n :aria-pressed=\"revealed\"\n tabindex=\"-1\"\n @click=\"revealed = !revealed\"\n >\n <wx-icon :name=\"revealed ? 'eye-off' : 'eye'\" />\n </button>\n </template>\n </wx-input>\n </wx-form-item>\n\n <wx-checkbox v-if=\"remember\" v-model=\"rememberMe\" :label=\"labels.remember\" />\n\n <wx-button\n type=\"primary\"\n native-type=\"submit\"\n size=\"lg\"\n block\n :loading=\"busy\"\n :disabled=\"throttled\"\n >\n {{ labels.submit }}\n </wx-button>\n </wx-form>\n </wx-card>\n</template>\n\n<style scoped>\n.wx-login {\n width: 100%;\n /* Wide enough for an email address, narrow enough to stay a card rather than a page. */\n max-width: 26rem;\n}\n\n.wx-login__message {\n margin-block-end: var(--wx-space-4);\n}\n\n.wx-login__reveal {\n display: inline-flex;\n align-items: center;\n padding: 0;\n color: var(--wx-text-muted);\n background: none;\n border: 0;\n cursor: pointer;\n}\n\n.wx-login__reveal:hover,\n.wx-login__reveal[aria-pressed='true'] {\n color: var(--wx-text-default);\n}\n</style>\n","import type { Messages } from '@webx-ui/admin'\n\n/**\n * What this package says, in English.\n *\n * The same keys `webx-ui/module-auth` ships as `lang/en/*.php`. Kept here so a card placed by\n * hand, with no WebX UI server behind it, still has words — and so a key never reaches the\n * screen when a translation is missing.\n */\nexport const authMessages: Record<string, Messages> = {\n card: {\n email: 'Email',\n password: 'Password',\n remember: 'Stay signed in',\n submit: 'Sign in',\n 'caps-lock': 'Caps Lock is on.',\n reveal: 'Show the password',\n hide: 'Hide the password',\n throttled: 'Too many attempts. Try again in :seconds s.',\n failed: 'Those details do not match an account.',\n },\n menu: {\n 'sign-out': 'Sign out',\n language: 'Interface language',\n },\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useAdmin, useI18n, useTranslate } from '@webx-ui/admin'\nimport { useAuth } from './session'\n\n/**\n * The corner of the header: who this is, which language they read the panel in, and the way\n * out.\n *\n * The language picker lives here rather than in a settings screen because it is a property of\n * the person, not of the site — and because somebody who has landed in a language they cannot\n * read needs it within reach, not three clicks into a section they cannot navigate.\n */\nwithDefaults(defineProps<{ signOutLabel?: string }>(), { signOutLabel: undefined })\n\nconst admin = useAdmin()\nconst auth = useAuth()\nconst i18n = useI18n()\nconst t = useTranslate('webx-auth')\n\nconst user = computed(() => admin.state.user)\n\n// One language is not a choice, and a menu that offers it is noise.\nconst languages = computed(() =>\n i18n.state.panelLocales.length > 1 ? i18n.state.panelLocales : [],\n)\n\nconst initials = computed(() => {\n const name = user.value?.name ?? ''\n\n return name\n .split(/\\s+/)\n .filter((part) => part !== '')\n .slice(0, 2)\n .map((part) => part[0]?.toUpperCase() ?? '')\n .join('')\n})\n\nasync function choose(code: string): Promise<void> {\n if (code === i18n.state.locale) {\n return\n }\n\n await auth.setLocale(code)\n}\n</script>\n\n<template>\n <wx-dropdown v-if=\"user !== null\">\n <template #trigger>\n <wx-action :title=\"user.name\">\n <wx-avatar :label=\"initials\" size=\"sm\" />\n </wx-action>\n </template>\n\n <wx-dropdown-item disabled>\n <wx-text size=\"sm\">{{ user.email }}</wx-text>\n </wx-dropdown-item>\n\n <template v-if=\"languages.length > 0\">\n <wx-divider :spacing=\"4\" />\n\n <wx-dropdown-item disabled>\n <wx-text size=\"sm\" tone=\"muted\">{{ t('menu.language') }}</wx-text>\n </wx-dropdown-item>\n\n <wx-dropdown-item\n v-for=\"language in languages\"\n :key=\"language.code\"\n :icon=\"language.code === i18n.state.locale ? 'check' : undefined\"\n @click=\"choose(language.code)\"\n >\n {{ language.nativeName }}\n </wx-dropdown-item>\n\n <wx-divider :spacing=\"4\" />\n </template>\n\n <wx-dropdown-item icon=\"logout\" @click=\"auth.logout()\">\n {{ signOutLabel ?? t('menu.sign-out') }}\n </wx-dropdown-item>\n </wx-dropdown>\n</template>\n","import { h, watch } from 'vue'\nimport type { Admin, AdminPlugin } from '@webx-ui/admin'\nimport LoginCard from './LoginCard.vue'\nimport { authMessages } from './messages'\nimport { createAuthSession, provideAuth } from './session'\n\nexport interface AuthOptions {\n /** Where the sign-in screen lives, inside the panel. */\n path?: string\n /** Props handed to the card — the labels, mostly. */\n card?: Record<string, unknown>\n}\n\n/**\n * Signing in, as a plugin rather than a section: it owns a route and a guard, and it has no\n * place in the menu.\n *\n * Installing it also tells the panel how to find out who is signed in, which is why the panel\n * itself knows nothing about authentication — it holds the answer, not the question.\n */\nexport function auth(options: AuthOptions = {}): AdminPlugin {\n const path = options.path ?? '/login'\n\n return {\n install(admin: Admin) {\n const session = createAuthSession(admin.context)\n\n // The floor under this module's labels: what the server has not translated, and what\n // a panel assembled without a server has to fall back on.\n admin.i18n.defaults('webx-auth', authMessages)\n\n provideAuth(admin.app, session)\n admin.context.useSessionLoader(() => session.me())\n\n admin.router.addRoute({\n path,\n name: 'webx.login',\n meta: { public: true },\n component: { render: () => h(LoginCard, { ...options.card }) },\n })\n\n admin.router.beforeEach((to) => {\n if (to.meta.public === true) {\n return true\n }\n\n if (admin.context.state.status !== 'unauthenticated') {\n return true\n }\n\n return { path, query: to.fullPath === '/' ? {} : { next: to.fullPath } }\n })\n\n // Both directions are driven by the state rather than by an event, because the card\n // cannot be relied on to announce its own success: signing in flips the panel to\n // `ready`, the shell swaps the branch it renders, and the card is unmounted before the\n // line after `await` runs.\n watch(\n () => admin.context.state.status,\n (status) => {\n const current = admin.router.currentRoute.value\n\n // A session can also end between requests — a sign-out in another tab, an account\n // switched off. The panel learns that from a 401 on whatever it asked for next,\n // which is not a navigation, so nothing would move without this.\n if (status === 'unauthenticated' && current.path !== path) {\n void admin.router.replace({ path, query: { next: current.fullPath } })\n\n return\n }\n\n if (status === 'ready' && current.path === path) {\n const next = current.query.next\n\n void admin.router.replace(typeof next === 'string' ? next : '/')\n }\n },\n )\n },\n }\n}\n\nexport { createAuthSession, provideAuth, useAuth, authKey } from './session'\nexport type { AuthSession, Credentials } from './session'\nexport { default as WxLoginCard } from './LoginCard.vue'\nexport { default as WxUserMenu } from './UserMenu.vue'\nexport { authMessages } from './messages'\n"],"names":["authKey","useAuth","session","inject","createAuthSession","admin","base","error","isUnauthenticated","credentials","body","code","provideAuth","app","props","__props","emit","__emit","auth","t","useTranslate","labels","computed","email","ref","password","rememberMe","busy","revealed","capsLock","message","errors","secondsLeft","emailField","useTemplateRef","countdown","onBeforeUnmount","stopCountdown","throttled","throttleNotice","submit","handle","HttpError","startCountdown","seconds","trackCapsLock","event","__expose","_createBlock","_component_wx_card","_createVNode","_component_wx_form","_component_wx_alert","_component_wx_form_item","_component_wx_input","$event","_component_wx_icon","_createElementVNode","_cache","_component_wx_checkbox","_component_wx_button","_createTextVNode","_toDisplayString","authMessages","useAdmin","i18n","useI18n","user","languages","initials","part","choose","_component_wx_dropdown","_component_wx_action","_component_wx_avatar","_component_wx_dropdown_item","_component_wx_text","_createElementBlock","_Fragment","_component_wx_divider","_unref","_renderList","language","options","path","h","LoginCard","to","watch","status","current","next"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Messages } from '@webx-ui/admin';
|
|
2
|
+
/**
|
|
3
|
+
* What this package says, in English.
|
|
4
|
+
*
|
|
5
|
+
* The same keys `webx-ui/module-auth` ships as `lang/en/*.php`. Kept here so a card placed by
|
|
6
|
+
* hand, with no WebX UI server behind it, still has words — and so a key never reaches the
|
|
7
|
+
* screen when a translation is missing.
|
|
8
|
+
*/
|
|
9
|
+
export declare const authMessages: Record<string, Messages>;
|
package/dist/session.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ export interface AuthSession {
|
|
|
11
11
|
/** Signs in and tells the panel to load itself. Throws `HttpError` on a refusal. */
|
|
12
12
|
login(credentials: Credentials): Promise<AdminUser>;
|
|
13
13
|
logout(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Change the language this administrator reads the panel in.
|
|
16
|
+
*
|
|
17
|
+
* Stored on the person rather than in this browser, so it follows them to the next machine
|
|
18
|
+
* and so the server writes its own messages — a 422 under a field — in the same language as
|
|
19
|
+
* the label above it.
|
|
20
|
+
*/
|
|
21
|
+
setLocale(code: string): Promise<void>;
|
|
14
22
|
}
|
|
15
23
|
export declare const authKey: InjectionKey<AuthSession>;
|
|
16
24
|
export declare function useAuth(): AuthSession;
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.wx-login[data-v-
|
|
1
|
+
.wx-login[data-v-1224c1f8]{width:100%;max-width:26rem}.wx-login__message[data-v-1224c1f8]{margin-block-end:var(--wx-space-4)}.wx-login__reveal[data-v-1224c1f8]{display:inline-flex;align-items:center;padding:0;color:var(--wx-text-muted);background:none;border:0;cursor:pointer}.wx-login__reveal[data-v-1224c1f8]:hover,.wx-login__reveal[aria-pressed=true][data-v-1224c1f8]{color:var(--wx-text-default)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webx-ui/module-auth",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Signing in to a WebX UI admin panel: the screen, the session and the router guard. Pairs with webx-ui/module-auth on the server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"vue-router": "^4.5.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@webx-ui/admin": "^0.0
|
|
48
|
-
"@webx-ui/core": "^0.14.
|
|
47
|
+
"@webx-ui/admin": "^0.2.0",
|
|
48
|
+
"@webx-ui/core": "^0.14.2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.10.1",
|