@webx-ui/module-auth 0.0.1 → 0.1.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/dist/index.js +88 -81
- package/dist/index.js.map +1 -1
- 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/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { inject as
|
|
1
|
+
import { inject as G, defineComponent as T, ref as d, useTemplateRef as J, onBeforeUnmount as Q, computed as k, resolveComponent as r, openBlock as g, createBlock as h, withCtx as s, createVNode as l, withModifiers as X, createCommentVNode as M, createElementVNode as Y, createTextVNode as $, toDisplayString as A, unref as Z, h as ee, watch as te } from "vue";
|
|
2
2
|
import { HttpError as ae, useAdmin as oe } from "@webx-ui/admin";
|
|
3
3
|
const W = /* @__PURE__ */ Symbol("webx-auth");
|
|
4
4
|
function j() {
|
|
5
|
-
const e =
|
|
5
|
+
const e = G(W, 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;
|
|
@@ -42,7 +42,7 @@ function le(e, a) {
|
|
|
42
42
|
function se(e) {
|
|
43
43
|
return typeof e == "object" && e !== null && "status" in e && e.status === 401;
|
|
44
44
|
}
|
|
45
|
-
const ue = /* @__PURE__ */
|
|
45
|
+
const ue = ["title", "aria-label", "aria-pressed"], re = /* @__PURE__ */ T({
|
|
46
46
|
__name: "LoginCard",
|
|
47
47
|
props: {
|
|
48
48
|
emailLabel: { default: "Email" },
|
|
@@ -56,29 +56,29 @@ const ue = /* @__PURE__ */ N({
|
|
|
56
56
|
},
|
|
57
57
|
emits: ["success"],
|
|
58
58
|
setup(e, { expose: a, emit: t }) {
|
|
59
|
-
const n = e, i = t,
|
|
59
|
+
const n = e, i = t, p = j(), u = d(""), v = d(""), _ = d(!1), f = d(!1), c = d(!1), y = d(!1), w = d(""), C = d({}), b = d(0), E = J("emailField");
|
|
60
60
|
let L = null;
|
|
61
|
-
|
|
62
|
-
const V = k(() =>
|
|
63
|
-
() => n.throttleMessage.replace("{seconds}", String(
|
|
61
|
+
Q(S);
|
|
62
|
+
const V = k(() => b.value > 0), q = k(
|
|
63
|
+
() => n.throttleMessage.replace("{seconds}", String(b.value))
|
|
64
64
|
);
|
|
65
65
|
async function F() {
|
|
66
66
|
if (!(f.value || V.value)) {
|
|
67
67
|
f.value = !0, w.value = "", C.value = {};
|
|
68
68
|
try {
|
|
69
|
-
await
|
|
70
|
-
email:
|
|
69
|
+
await p.login({
|
|
70
|
+
email: u.value,
|
|
71
71
|
password: v.value,
|
|
72
72
|
remember: _.value
|
|
73
73
|
}), v.value = "", i("success");
|
|
74
74
|
} catch (o) {
|
|
75
|
-
|
|
75
|
+
H(o);
|
|
76
76
|
} finally {
|
|
77
77
|
f.value = !1;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
function
|
|
81
|
+
function H(o) {
|
|
82
82
|
if (!(o instanceof ae)) {
|
|
83
83
|
w.value = o instanceof Error ? o.message : String(o);
|
|
84
84
|
return;
|
|
@@ -88,36 +88,36 @@ const ue = /* @__PURE__ */ N({
|
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
if (o.isThrottled) {
|
|
91
|
-
|
|
91
|
+
I(o.retryAfter ?? 60);
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
w.value = o.message;
|
|
95
95
|
}
|
|
96
|
-
function
|
|
97
|
-
S(),
|
|
98
|
-
|
|
96
|
+
function I(o) {
|
|
97
|
+
S(), b.value = o, L = setInterval(() => {
|
|
98
|
+
b.value -= 1, b.value <= 0 && S();
|
|
99
99
|
}, 1e3);
|
|
100
100
|
}
|
|
101
101
|
function S() {
|
|
102
|
-
L !== null && (clearInterval(L), L = null),
|
|
102
|
+
L !== null && (clearInterval(L), L = null), b.value = 0;
|
|
103
103
|
}
|
|
104
104
|
function z(o) {
|
|
105
105
|
y.value = o.getModifierState("CapsLock");
|
|
106
106
|
}
|
|
107
|
-
return a({ focus: () =>
|
|
108
|
-
const B =
|
|
109
|
-
return g(), h(
|
|
107
|
+
return a({ focus: () => E.value?.focus() }), (o, m) => {
|
|
108
|
+
const B = r("wx-alert"), U = r("wx-icon"), N = r("wx-input"), P = r("wx-form-item"), K = r("wx-checkbox"), O = r("wx-button"), R = r("wx-form"), D = r("wx-card");
|
|
109
|
+
return g(), h(D, {
|
|
110
110
|
class: "wx-login",
|
|
111
111
|
padding: 24
|
|
112
112
|
}, {
|
|
113
|
-
default:
|
|
114
|
-
|
|
113
|
+
default: s(() => [
|
|
114
|
+
l(R, {
|
|
115
115
|
errors: C.value,
|
|
116
116
|
disabled: f.value,
|
|
117
117
|
gap: "md",
|
|
118
|
-
onSubmit:
|
|
118
|
+
onSubmit: X(F, ["prevent"])
|
|
119
119
|
}, {
|
|
120
|
-
default:
|
|
120
|
+
default: s(() => [
|
|
121
121
|
w.value !== "" ? (g(), h(B, {
|
|
122
122
|
key: 0,
|
|
123
123
|
type: "danger",
|
|
@@ -132,14 +132,14 @@ const ue = /* @__PURE__ */ N({
|
|
|
132
132
|
live: "",
|
|
133
133
|
description: q.value,
|
|
134
134
|
class: "wx-login__message"
|
|
135
|
-
}, null, 8, ["description"])) :
|
|
136
|
-
|
|
137
|
-
default:
|
|
138
|
-
|
|
135
|
+
}, null, 8, ["description"])) : M("", !0),
|
|
136
|
+
l(P, { name: "email" }, {
|
|
137
|
+
default: s(() => [
|
|
138
|
+
l(N, {
|
|
139
139
|
ref_key: "emailField",
|
|
140
|
-
ref:
|
|
141
|
-
modelValue:
|
|
142
|
-
"onUpdate:modelValue":
|
|
140
|
+
ref: E,
|
|
141
|
+
modelValue: u.value,
|
|
142
|
+
"onUpdate:modelValue": m[0] || (m[0] = (x) => u.value = x),
|
|
143
143
|
type: "email",
|
|
144
144
|
placeholder: e.emailLabel,
|
|
145
145
|
"aria-label": e.emailLabel,
|
|
@@ -148,23 +148,23 @@ const ue = /* @__PURE__ */ N({
|
|
|
148
148
|
inputmode: "email",
|
|
149
149
|
size: "lg"
|
|
150
150
|
}, {
|
|
151
|
-
prefix:
|
|
152
|
-
|
|
151
|
+
prefix: s(() => [
|
|
152
|
+
l(U, { name: "mail" })
|
|
153
153
|
]),
|
|
154
154
|
_: 1
|
|
155
155
|
}, 8, ["modelValue", "placeholder", "aria-label", "autofocus"])
|
|
156
156
|
]),
|
|
157
157
|
_: 1
|
|
158
158
|
}),
|
|
159
|
-
|
|
159
|
+
l(P, {
|
|
160
160
|
name: "password",
|
|
161
161
|
help: y.value ? e.capsLockWarning : void 0
|
|
162
162
|
}, {
|
|
163
|
-
default:
|
|
164
|
-
|
|
163
|
+
default: s(() => [
|
|
164
|
+
l(N, {
|
|
165
165
|
modelValue: v.value,
|
|
166
|
-
"onUpdate:modelValue":
|
|
167
|
-
type:
|
|
166
|
+
"onUpdate:modelValue": m[2] || (m[2] = (x) => v.value = x),
|
|
167
|
+
type: c.value ? "text" : "password",
|
|
168
168
|
placeholder: e.passwordLabel,
|
|
169
169
|
"aria-label": e.passwordLabel,
|
|
170
170
|
autocomplete: "current-password",
|
|
@@ -172,29 +172,36 @@ const ue = /* @__PURE__ */ N({
|
|
|
172
172
|
onKeydown: z,
|
|
173
173
|
onKeyup: z
|
|
174
174
|
}, {
|
|
175
|
-
prefix:
|
|
176
|
-
|
|
175
|
+
prefix: s(() => [
|
|
176
|
+
l(U, { name: "lock" })
|
|
177
177
|
]),
|
|
178
|
-
suffix:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
suffix: s(() => [
|
|
179
|
+
Y("button", {
|
|
180
|
+
type: "button",
|
|
181
|
+
class: "wx-login__reveal",
|
|
182
|
+
title: c.value ? "Hide the password" : "Show the password",
|
|
183
|
+
"aria-label": c.value ? "Hide the password" : "Show the password",
|
|
184
|
+
"aria-pressed": c.value,
|
|
182
185
|
tabindex: "-1",
|
|
183
|
-
onClick:
|
|
184
|
-
},
|
|
186
|
+
onClick: m[1] || (m[1] = (x) => c.value = !c.value)
|
|
187
|
+
}, [
|
|
188
|
+
l(U, {
|
|
189
|
+
name: c.value ? "eye-off" : "eye"
|
|
190
|
+
}, null, 8, ["name"])
|
|
191
|
+
], 8, ue)
|
|
185
192
|
]),
|
|
186
193
|
_: 1
|
|
187
194
|
}, 8, ["modelValue", "type", "placeholder", "aria-label"])
|
|
188
195
|
]),
|
|
189
196
|
_: 1
|
|
190
197
|
}, 8, ["help"]),
|
|
191
|
-
e.remember ? (g(), h(
|
|
198
|
+
e.remember ? (g(), h(K, {
|
|
192
199
|
key: 2,
|
|
193
200
|
modelValue: _.value,
|
|
194
|
-
"onUpdate:modelValue":
|
|
201
|
+
"onUpdate:modelValue": m[3] || (m[3] = (x) => _.value = x),
|
|
195
202
|
label: e.rememberLabel
|
|
196
|
-
}, null, 8, ["modelValue", "label"])) :
|
|
197
|
-
|
|
203
|
+
}, null, 8, ["modelValue", "label"])) : M("", !0),
|
|
204
|
+
l(O, {
|
|
198
205
|
type: "primary",
|
|
199
206
|
"native-type": "submit",
|
|
200
207
|
size: "lg",
|
|
@@ -202,8 +209,8 @@ const ue = /* @__PURE__ */ N({
|
|
|
202
209
|
loading: f.value,
|
|
203
210
|
disabled: V.value
|
|
204
211
|
}, {
|
|
205
|
-
default:
|
|
206
|
-
|
|
212
|
+
default: s(() => [
|
|
213
|
+
$(A(e.submitLabel), 1)
|
|
207
214
|
]),
|
|
208
215
|
_: 1
|
|
209
216
|
}, 8, ["loading", "disabled"])
|
|
@@ -215,27 +222,27 @@ const ue = /* @__PURE__ */ N({
|
|
|
215
222
|
});
|
|
216
223
|
};
|
|
217
224
|
}
|
|
218
|
-
}),
|
|
225
|
+
}), ie = (e, a) => {
|
|
219
226
|
const t = e.__vccOpts || e;
|
|
220
227
|
for (const [n, i] of a)
|
|
221
228
|
t[n] = i;
|
|
222
229
|
return t;
|
|
223
|
-
},
|
|
230
|
+
}, ce = /* @__PURE__ */ ie(re, [["__scopeId", "data-v-41458fe2"]]), me = /* @__PURE__ */ T({
|
|
224
231
|
__name: "UserMenu",
|
|
225
232
|
props: {
|
|
226
233
|
signOutLabel: { default: "Sign out" }
|
|
227
234
|
},
|
|
228
235
|
setup(e) {
|
|
229
|
-
const a = oe(), t = j(), n = k(() => a.state.user), i = k(() => (n.value?.name ?? "").split(/\s+/).filter((
|
|
230
|
-
return (
|
|
231
|
-
const v =
|
|
236
|
+
const a = oe(), t = j(), n = k(() => a.state.user), i = k(() => (n.value?.name ?? "").split(/\s+/).filter((u) => u !== "").slice(0, 2).map((u) => u[0]?.toUpperCase() ?? "").join(""));
|
|
237
|
+
return (p, u) => {
|
|
238
|
+
const v = r("wx-avatar"), _ = r("wx-action"), f = r("wx-text"), c = r("wx-dropdown-item"), y = r("wx-dropdown");
|
|
232
239
|
return n.value !== null ? (g(), h(y, { key: 0 }, {
|
|
233
|
-
trigger:
|
|
234
|
-
|
|
240
|
+
trigger: s(() => [
|
|
241
|
+
l(_, {
|
|
235
242
|
title: n.value.name
|
|
236
243
|
}, {
|
|
237
|
-
default:
|
|
238
|
-
|
|
244
|
+
default: s(() => [
|
|
245
|
+
l(v, {
|
|
239
246
|
label: i.value,
|
|
240
247
|
size: "sm"
|
|
241
248
|
}, null, 8, ["label"])
|
|
@@ -243,34 +250,34 @@ const ue = /* @__PURE__ */ N({
|
|
|
243
250
|
_: 1
|
|
244
251
|
}, 8, ["title"])
|
|
245
252
|
]),
|
|
246
|
-
default:
|
|
247
|
-
|
|
248
|
-
default:
|
|
249
|
-
|
|
250
|
-
default:
|
|
251
|
-
|
|
253
|
+
default: s(() => [
|
|
254
|
+
l(c, { disabled: "" }, {
|
|
255
|
+
default: s(() => [
|
|
256
|
+
l(f, { size: "sm" }, {
|
|
257
|
+
default: s(() => [
|
|
258
|
+
$(A(n.value.email), 1)
|
|
252
259
|
]),
|
|
253
260
|
_: 1
|
|
254
261
|
})
|
|
255
262
|
]),
|
|
256
263
|
_: 1
|
|
257
264
|
}),
|
|
258
|
-
|
|
265
|
+
l(c, {
|
|
259
266
|
icon: "logout",
|
|
260
|
-
onClick:
|
|
267
|
+
onClick: u[0] || (u[0] = (w) => Z(t).logout())
|
|
261
268
|
}, {
|
|
262
|
-
default:
|
|
263
|
-
|
|
269
|
+
default: s(() => [
|
|
270
|
+
$(A(e.signOutLabel), 1)
|
|
264
271
|
]),
|
|
265
272
|
_: 1
|
|
266
273
|
})
|
|
267
274
|
]),
|
|
268
275
|
_: 1
|
|
269
|
-
})) :
|
|
276
|
+
})) : M("", !0);
|
|
270
277
|
};
|
|
271
278
|
}
|
|
272
279
|
});
|
|
273
|
-
function
|
|
280
|
+
function fe(e = {}) {
|
|
274
281
|
const a = e.path ?? "/login";
|
|
275
282
|
return {
|
|
276
283
|
install(t) {
|
|
@@ -279,18 +286,18 @@ function pe(e = {}) {
|
|
|
279
286
|
path: a,
|
|
280
287
|
name: "webx.login",
|
|
281
288
|
meta: { public: !0 },
|
|
282
|
-
component: { render: () => ee(
|
|
289
|
+
component: { render: () => ee(ce, { ...e.card }) }
|
|
283
290
|
}), t.router.beforeEach((i) => i.meta.public === !0 || t.context.state.status !== "unauthenticated" ? !0 : { path: a, query: i.fullPath === "/" ? {} : { next: i.fullPath } }), te(
|
|
284
291
|
() => t.context.state.status,
|
|
285
292
|
(i) => {
|
|
286
|
-
const
|
|
287
|
-
if (i === "unauthenticated" &&
|
|
288
|
-
t.router.replace({ path: a, query: { next:
|
|
293
|
+
const p = t.router.currentRoute.value;
|
|
294
|
+
if (i === "unauthenticated" && p.path !== a) {
|
|
295
|
+
t.router.replace({ path: a, query: { next: p.fullPath } });
|
|
289
296
|
return;
|
|
290
297
|
}
|
|
291
|
-
if (i === "ready" &&
|
|
292
|
-
const
|
|
293
|
-
t.router.replace(typeof
|
|
298
|
+
if (i === "ready" && p.path === a) {
|
|
299
|
+
const u = p.query.next;
|
|
300
|
+
t.router.replace(typeof u == "string" ? u : "/");
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
303
|
);
|
|
@@ -298,9 +305,9 @@ function pe(e = {}) {
|
|
|
298
305
|
};
|
|
299
306
|
}
|
|
300
307
|
export {
|
|
301
|
-
|
|
308
|
+
ce as WxLoginCard,
|
|
302
309
|
me as WxUserMenu,
|
|
303
|
-
|
|
310
|
+
fe as auth,
|
|
304
311
|
W as authKey,
|
|
305
312
|
ne as createAuthSession,
|
|
306
313
|
le as provideAuth,
|
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/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 <!-- 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 ? 'Hide the password' : 'Show the password'\"\n :aria-label=\"revealed ? 'Hide the password' : 'Show the password'\"\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=\"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\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","<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","_createElementVNode","_cache","_component_wx_checkbox","_component_wx_button","useAdmin","user","initials","part","_component_wx_dropdown","_component_wx_action","_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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"}
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.wx-login[data-v-
|
|
1
|
+
.wx-login[data-v-41458fe2]{width:100%;max-width:26rem}.wx-login__message[data-v-41458fe2]{margin-block-end:var(--wx-space-4)}.wx-login__reveal[data-v-41458fe2]{display:inline-flex;align-items:center;padding:0;color:var(--wx-text-muted);background:none;border:0;cursor:pointer}.wx-login__reveal[data-v-41458fe2]:hover,.wx-login__reveal[aria-pressed=true][data-v-41458fe2]{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.1.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.1.0",
|
|
48
|
+
"@webx-ui/core": "^0.14.2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.10.1",
|