@jskit-ai/auth-web 0.1.38 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +5 -5
- package/package.json +6 -5
- package/src/client/composables/loginView/constants.js +1 -1
- package/src/client/index.js +2 -0
- package/src/client/providers/AuthWebClientProvider.js +2 -17
- package/src/client/providers/bootAuthClientProvider.js +30 -0
- package/src/client/runtime/inject.js +5 -1
- package/src/client/stores/useAuthStore.js +85 -0
- package/src/client/views/AuthProfileWidget.vue +5 -22
- package/test/clientBoot.test.js +3 -0
- package/test/provider.test.js +165 -0
- package/test/useAuth.test.js +95 -0
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
"packageVersion": 1,
|
|
3
3
|
"packageId": "@jskit-ai/auth-web",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.39",
|
|
5
5
|
"kind": "runtime",
|
|
6
6
|
"description": "Auth web module: Fastify auth routes plus web login/sign-out scaffolds.",
|
|
7
7
|
"dependsOn": [
|
|
@@ -220,10 +220,10 @@ export default Object.freeze({
|
|
|
220
220
|
"@tanstack/vue-query": "5.92.12",
|
|
221
221
|
"@mdi/js": "^7.4.47",
|
|
222
222
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
223
|
-
"@jskit-ai/auth-core": "0.1.
|
|
224
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
225
|
-
"@jskit-ai/kernel": "0.1.
|
|
226
|
-
"@jskit-ai/shell-web": "0.1.
|
|
223
|
+
"@jskit-ai/auth-core": "0.1.37",
|
|
224
|
+
"@jskit-ai/http-runtime": "0.1.37",
|
|
225
|
+
"@jskit-ai/kernel": "0.1.38",
|
|
226
|
+
"@jskit-ai/shell-web": "0.1.37",
|
|
227
227
|
"vuetify": "^4.0.0"
|
|
228
228
|
},
|
|
229
229
|
"dev": {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/auth-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@tanstack/vue-query": "^5.90.5",
|
|
21
|
-
"@jskit-ai/auth-core": "0.1.
|
|
21
|
+
"@jskit-ai/auth-core": "0.1.37",
|
|
22
22
|
"@mdi/js": "^7.4.47",
|
|
23
23
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
24
|
-
"@jskit-ai/kernel": "0.1.
|
|
25
|
-
"@jskit-ai/shell-web": "0.1.
|
|
24
|
+
"@jskit-ai/kernel": "0.1.38",
|
|
25
|
+
"@jskit-ai/shell-web": "0.1.37",
|
|
26
|
+
"pinia": "^3.0.4",
|
|
26
27
|
"vuetify": "^4.0.0",
|
|
27
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
28
|
+
"@jskit-ai/http-runtime": "0.1.37"
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -5,7 +5,7 @@ const OTP_MODE = "otp";
|
|
|
5
5
|
const EMAIL_CONFIRMATION_MODE = "confirm-email";
|
|
6
6
|
|
|
7
7
|
const AUTH_TITLE_BY_MODE = Object.freeze({
|
|
8
|
-
[LOGIN_MODE]: "Welcome
|
|
8
|
+
[LOGIN_MODE]: "Welcome",
|
|
9
9
|
[REGISTER_MODE]: "Create your account",
|
|
10
10
|
[FORGOT_MODE]: "Reset your password",
|
|
11
11
|
[OTP_MODE]: "Use one-time code",
|
package/src/client/index.js
CHANGED
|
@@ -7,6 +7,8 @@ export { default as DefaultLoginView } from "./views/DefaultLoginView.vue";
|
|
|
7
7
|
export { default as DefaultSignOutView } from "./views/DefaultSignOutView.vue";
|
|
8
8
|
export { default as AuthProfileWidget } from "./views/AuthProfileWidget.vue";
|
|
9
9
|
export { default as AuthProfileMenuLinkItem } from "./views/AuthProfileMenuLinkItem.vue";
|
|
10
|
+
export { useAuthStore } from "./stores/useAuthStore.js";
|
|
11
|
+
export { useAuthGuardRuntime } from "./runtime/inject.js";
|
|
10
12
|
|
|
11
13
|
const routeComponents = Object.freeze({
|
|
12
14
|
"auth-login": DefaultLoginView,
|
|
@@ -3,6 +3,7 @@ import AuthProfileWidget from "../views/AuthProfileWidget.vue";
|
|
|
3
3
|
import AuthProfileMenuLinkItem from "../views/AuthProfileMenuLinkItem.vue";
|
|
4
4
|
import { createAuthGuardRuntime } from "../runtime/authGuardRuntime.js";
|
|
5
5
|
import { useLoginView } from "../runtime/useLoginView.js";
|
|
6
|
+
import { bootAuthClientProvider } from "./bootAuthClientProvider.js";
|
|
6
7
|
import { resolveSurfaceNavigationTargetFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
7
8
|
|
|
8
9
|
class AuthWebClientProvider {
|
|
@@ -37,23 +38,7 @@ class AuthWebClientProvider {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
async boot(app) {
|
|
40
|
-
|
|
41
|
-
throw new Error("AuthWebClientProvider requires application make().");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const authGuardRuntime = app.make("runtime.auth-guard.client");
|
|
45
|
-
await authGuardRuntime.initialize();
|
|
46
|
-
|
|
47
|
-
if (!app.has("jskit.client.vue.app")) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const vueApp = app.make("jskit.client.vue.app");
|
|
52
|
-
if (!vueApp || typeof vueApp.provide !== "function") {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
vueApp.provide("jskit.auth-web.runtime.auth-guard.client", authGuardRuntime);
|
|
41
|
+
await bootAuthClientProvider(app);
|
|
57
42
|
}
|
|
58
43
|
}
|
|
59
44
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AUTH_GUARD_RUNTIME_INJECTION_KEY } from "../runtime/inject.js";
|
|
2
|
+
import { useAuthStore } from "../stores/useAuthStore.js";
|
|
3
|
+
|
|
4
|
+
async function bootAuthClientProvider(app) {
|
|
5
|
+
if (!app || typeof app.make !== "function") {
|
|
6
|
+
throw new Error("AuthWebClientProvider requires application make().");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const authGuardRuntime = app.make("runtime.auth-guard.client");
|
|
10
|
+
const pinia = app.make("jskit.client.pinia");
|
|
11
|
+
if (!pinia) {
|
|
12
|
+
throw new Error("AuthWebClientProvider requires Pinia installed in the client app.");
|
|
13
|
+
}
|
|
14
|
+
const authStore = useAuthStore(pinia);
|
|
15
|
+
authStore.attachRuntime(authGuardRuntime);
|
|
16
|
+
await authStore.initialize();
|
|
17
|
+
|
|
18
|
+
if (!app.has("jskit.client.vue.app")) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const vueApp = app.make("jskit.client.vue.app");
|
|
23
|
+
if (!vueApp || typeof vueApp.provide !== "function") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
vueApp.provide(AUTH_GUARD_RUNTIME_INJECTION_KEY, authGuardRuntime);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { bootAuthClientProvider };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { inject } from "vue";
|
|
2
2
|
import { isAuthGuardRuntime } from "./authGuardRuntime.js";
|
|
3
3
|
|
|
4
|
+
const AUTH_GUARD_RUNTIME_INJECTION_KEY = "jskit.auth-web.runtime.auth-guard.client";
|
|
5
|
+
|
|
4
6
|
const EMPTY_AUTH_GUARD_STATE = Object.freeze({
|
|
5
7
|
authenticated: false,
|
|
6
8
|
username: "",
|
|
@@ -24,7 +26,7 @@ const EMPTY_AUTH_GUARD_RUNTIME = Object.freeze({
|
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
function useAuthGuardRuntime({ required = false } = {}) {
|
|
27
|
-
const runtime = inject(
|
|
29
|
+
const runtime = inject(AUTH_GUARD_RUNTIME_INJECTION_KEY, null);
|
|
28
30
|
if (isAuthGuardRuntime(runtime)) {
|
|
29
31
|
return runtime;
|
|
30
32
|
}
|
|
@@ -37,6 +39,8 @@ function useAuthGuardRuntime({ required = false } = {}) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export {
|
|
42
|
+
AUTH_GUARD_RUNTIME_INJECTION_KEY,
|
|
43
|
+
EMPTY_AUTH_GUARD_STATE,
|
|
40
44
|
EMPTY_AUTH_GUARD_RUNTIME,
|
|
41
45
|
useAuthGuardRuntime
|
|
42
46
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { computed, markRaw, shallowRef } from "vue";
|
|
2
|
+
import { defineStore } from "pinia";
|
|
3
|
+
import { isAuthGuardRuntime } from "../runtime/authGuardRuntime.js";
|
|
4
|
+
import { EMPTY_AUTH_GUARD_RUNTIME, EMPTY_AUTH_GUARD_STATE } from "../runtime/inject.js";
|
|
5
|
+
|
|
6
|
+
function normalizeAuthStateValue(nextState) {
|
|
7
|
+
if (!nextState || typeof nextState !== "object") {
|
|
8
|
+
return EMPTY_AUTH_GUARD_STATE;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return nextState;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useAuthStore = defineStore("jskit.auth-web.auth", () => {
|
|
15
|
+
const runtime = shallowRef(markRaw(EMPTY_AUTH_GUARD_RUNTIME));
|
|
16
|
+
const authState = shallowRef(EMPTY_AUTH_GUARD_STATE);
|
|
17
|
+
let unsubscribe = null;
|
|
18
|
+
|
|
19
|
+
function setAuthState(nextState) {
|
|
20
|
+
authState.value = normalizeAuthStateValue(nextState);
|
|
21
|
+
return authState.value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function detachRuntimeSubscription() {
|
|
25
|
+
if (typeof unsubscribe === "function") {
|
|
26
|
+
unsubscribe();
|
|
27
|
+
unsubscribe = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function attachRuntime(nextRuntime = EMPTY_AUTH_GUARD_RUNTIME) {
|
|
32
|
+
if (!isAuthGuardRuntime(nextRuntime) || typeof nextRuntime.subscribe !== "function") {
|
|
33
|
+
throw new TypeError("useAuthStore.attachRuntime requires an auth guard runtime with subscribe().");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (runtime.value === nextRuntime) {
|
|
37
|
+
setAuthState(nextRuntime.getState());
|
|
38
|
+
return runtime.value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
detachRuntimeSubscription();
|
|
42
|
+
runtime.value = markRaw(nextRuntime);
|
|
43
|
+
setAuthState(nextRuntime.getState());
|
|
44
|
+
unsubscribe = nextRuntime.subscribe((nextState) => {
|
|
45
|
+
setAuthState(nextState);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return runtime.value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function initialize(options = {}) {
|
|
52
|
+
return setAuthState(await runtime.value.initialize(options));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function refresh(options = {}) {
|
|
56
|
+
return setAuthState(await runtime.value.refresh(options));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getState() {
|
|
60
|
+
return authState.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function subscribe(listener) {
|
|
64
|
+
return runtime.value.subscribe(listener);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const authenticated = computed(() => authState.value.authenticated === true);
|
|
68
|
+
const username = computed(() => String(authState.value.username || ""));
|
|
69
|
+
const oauthProviders = computed(() => authState.value.oauthProviders || EMPTY_AUTH_GUARD_STATE.oauthProviders);
|
|
70
|
+
const oauthDefaultProvider = computed(() => String(authState.value.oauthDefaultProvider || ""));
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
runtime,
|
|
74
|
+
authState,
|
|
75
|
+
authenticated,
|
|
76
|
+
username,
|
|
77
|
+
oauthProviders,
|
|
78
|
+
oauthDefaultProvider,
|
|
79
|
+
attachRuntime,
|
|
80
|
+
initialize,
|
|
81
|
+
refresh,
|
|
82
|
+
getState,
|
|
83
|
+
subscribe
|
|
84
|
+
};
|
|
85
|
+
});
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed
|
|
2
|
+
import { computed } from "vue";
|
|
3
3
|
import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
4
4
|
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
5
|
-
import {
|
|
5
|
+
import { useAuthStore } from "../stores/useAuthStore.js";
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
required: true
|
|
9
|
-
});
|
|
10
|
-
const authState = ref(authGuardRuntime.getState());
|
|
7
|
+
const auth = useAuthStore();
|
|
11
8
|
const { context: shellPlacementContext } = useWebPlacementContext();
|
|
12
|
-
let unsubscribe = null;
|
|
13
9
|
|
|
14
10
|
const shellUser = computed(() => {
|
|
15
11
|
const user = shellPlacementContext.value?.user;
|
|
@@ -25,7 +21,7 @@ const displayName = computed(() => {
|
|
|
25
21
|
return fromContext;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
const username = String(
|
|
24
|
+
const username = String(auth.username || "").trim();
|
|
29
25
|
if (username) {
|
|
30
26
|
return username;
|
|
31
27
|
}
|
|
@@ -58,23 +54,10 @@ const initials = computed(() => {
|
|
|
58
54
|
|
|
59
55
|
const placementContext = computed(() => {
|
|
60
56
|
return {
|
|
61
|
-
auth: authState
|
|
57
|
+
auth: auth.authState,
|
|
62
58
|
user: shellUser.value
|
|
63
59
|
};
|
|
64
60
|
});
|
|
65
|
-
|
|
66
|
-
onMounted(() => {
|
|
67
|
-
unsubscribe = authGuardRuntime.subscribe((nextState) => {
|
|
68
|
-
authState.value = nextState;
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
onBeforeUnmount(() => {
|
|
73
|
-
if (typeof unsubscribe === "function") {
|
|
74
|
-
unsubscribe();
|
|
75
|
-
unsubscribe = null;
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
61
|
</script>
|
|
79
62
|
|
|
80
63
|
<template>
|
package/test/clientBoot.test.js
CHANGED
|
@@ -6,6 +6,9 @@ import test from "node:test";
|
|
|
6
6
|
test("auth-web client index defines provider-based client routes surface", () => {
|
|
7
7
|
const source = readFileSync(fileURLToPath(new URL("../src/client/index.js", import.meta.url)), "utf8");
|
|
8
8
|
|
|
9
|
+
assert.equal(source.includes('export { useAuthStore } from "./stores/useAuthStore.js";'), true);
|
|
10
|
+
assert.equal(source.includes('export { useAuthGuardRuntime } from "./runtime/inject.js";'), true);
|
|
11
|
+
assert.equal(source.includes('export { useAuth } from "./composables/useAuth.js";'), false);
|
|
9
12
|
assert.equal(source.includes("const routeComponents = Object.freeze({"), true);
|
|
10
13
|
assert.equal(source.includes('"auth-login": DefaultLoginView'), true);
|
|
11
14
|
assert.equal(source.includes('"auth-signout": DefaultSignOutView'), true);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createPinia } from "pinia";
|
|
4
|
+
import { AUTH_GUARD_RUNTIME_INJECTION_KEY } from "../src/client/runtime/inject.js";
|
|
5
|
+
import { bootAuthClientProvider } from "../src/client/providers/bootAuthClientProvider.js";
|
|
6
|
+
import { useAuthStore } from "../src/client/stores/useAuthStore.js";
|
|
7
|
+
|
|
8
|
+
function createAuthRuntimeStub(initialState = {}) {
|
|
9
|
+
let state = Object.freeze({
|
|
10
|
+
authenticated: Boolean(initialState.authenticated),
|
|
11
|
+
username: String(initialState.username || ""),
|
|
12
|
+
oauthDefaultProvider: String(initialState.oauthDefaultProvider || ""),
|
|
13
|
+
oauthProviders: Array.isArray(initialState.oauthProviders)
|
|
14
|
+
? Object.freeze([...initialState.oauthProviders])
|
|
15
|
+
: Object.freeze([])
|
|
16
|
+
});
|
|
17
|
+
let initializeCalls = 0;
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
async initialize() {
|
|
22
|
+
initializeCalls += 1;
|
|
23
|
+
return state;
|
|
24
|
+
},
|
|
25
|
+
async refresh() {
|
|
26
|
+
return state;
|
|
27
|
+
},
|
|
28
|
+
getState() {
|
|
29
|
+
return state;
|
|
30
|
+
},
|
|
31
|
+
subscribe(listener) {
|
|
32
|
+
if (typeof listener === "function") {
|
|
33
|
+
listeners.add(listener);
|
|
34
|
+
}
|
|
35
|
+
return () => {
|
|
36
|
+
listeners.delete(listener);
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
push(nextState = {}) {
|
|
40
|
+
state = Object.freeze({
|
|
41
|
+
authenticated: Boolean(nextState.authenticated),
|
|
42
|
+
username: String(nextState.username || ""),
|
|
43
|
+
oauthDefaultProvider: String(nextState.oauthDefaultProvider || ""),
|
|
44
|
+
oauthProviders: Array.isArray(nextState.oauthProviders)
|
|
45
|
+
? Object.freeze([...nextState.oauthProviders])
|
|
46
|
+
: Object.freeze([])
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for (const listener of listeners) {
|
|
50
|
+
listener(state);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
get initializeCalls() {
|
|
54
|
+
return initializeCalls;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createAppDouble({ authGuardRuntime } = {}) {
|
|
60
|
+
const singletons = new Map();
|
|
61
|
+
const singletonInstances = new Map();
|
|
62
|
+
const provided = [];
|
|
63
|
+
const pinia = createPinia();
|
|
64
|
+
const vueApp = {
|
|
65
|
+
provide(key, value) {
|
|
66
|
+
provided.push({ key, value });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
singletons,
|
|
72
|
+
provided,
|
|
73
|
+
pinia,
|
|
74
|
+
vueApp,
|
|
75
|
+
singleton(token, factory) {
|
|
76
|
+
singletons.set(token, factory);
|
|
77
|
+
},
|
|
78
|
+
has(token) {
|
|
79
|
+
if (token === "jskit.client.vue.app") {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (token === "jskit.client.pinia") {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (token === "runtime.web-placement.client") {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (token === "runtime.auth-guard.client") {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return singletons.has(token) || singletonInstances.has(token);
|
|
92
|
+
},
|
|
93
|
+
make(token) {
|
|
94
|
+
if (token === "jskit.client.vue.app") {
|
|
95
|
+
return vueApp;
|
|
96
|
+
}
|
|
97
|
+
if (token === "jskit.client.pinia") {
|
|
98
|
+
return pinia;
|
|
99
|
+
}
|
|
100
|
+
if (token === "runtime.web-placement.client") {
|
|
101
|
+
return {
|
|
102
|
+
getContext() {
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
surfaceConfig: Object.freeze({
|
|
105
|
+
enabledSurfaceIds: Object.freeze(["home"]),
|
|
106
|
+
defaultSurfaceId: "home",
|
|
107
|
+
surfaces: Object.freeze({
|
|
108
|
+
home: Object.freeze({
|
|
109
|
+
id: "home",
|
|
110
|
+
origin: "",
|
|
111
|
+
pagesRoot: "home"
|
|
112
|
+
}),
|
|
113
|
+
auth: Object.freeze({
|
|
114
|
+
id: "auth",
|
|
115
|
+
origin: "",
|
|
116
|
+
pagesRoot: "auth"
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (token === "runtime.auth-guard.client") {
|
|
125
|
+
return authGuardRuntime;
|
|
126
|
+
}
|
|
127
|
+
if (singletonInstances.has(token)) {
|
|
128
|
+
return singletonInstances.get(token);
|
|
129
|
+
}
|
|
130
|
+
const factory = singletons.get(token);
|
|
131
|
+
if (!factory) {
|
|
132
|
+
throw new Error(`Unknown token ${String(token)}`);
|
|
133
|
+
}
|
|
134
|
+
const instance = factory(this);
|
|
135
|
+
singletonInstances.set(token, instance);
|
|
136
|
+
return instance;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
test("auth web client boot binds explicit Pinia store state and raw runtime injection together", async () => {
|
|
142
|
+
const authGuardRuntime = createAuthRuntimeStub({
|
|
143
|
+
authenticated: true,
|
|
144
|
+
username: "ada"
|
|
145
|
+
});
|
|
146
|
+
const app = createAppDouble({ authGuardRuntime });
|
|
147
|
+
|
|
148
|
+
await bootAuthClientProvider(app);
|
|
149
|
+
|
|
150
|
+
const authStore = useAuthStore(app.pinia);
|
|
151
|
+
assert.equal(authStore.runtime, authGuardRuntime);
|
|
152
|
+
assert.equal(authStore.authenticated, true);
|
|
153
|
+
assert.equal(authStore.username, "ada");
|
|
154
|
+
assert.equal(authGuardRuntime.initializeCalls, 1);
|
|
155
|
+
|
|
156
|
+
authGuardRuntime.push({
|
|
157
|
+
authenticated: true,
|
|
158
|
+
username: "grace"
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
assert.equal(authStore.username, "grace");
|
|
162
|
+
|
|
163
|
+
const providedByKey = new Map(app.provided.map((entry) => [entry.key, entry.value]));
|
|
164
|
+
assert.equal(providedByKey.get(AUTH_GUARD_RUNTIME_INJECTION_KEY), authGuardRuntime);
|
|
165
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createPinia } from "pinia";
|
|
4
|
+
import { useAuthStore } from "../src/client/stores/useAuthStore.js";
|
|
5
|
+
|
|
6
|
+
function createAuthRuntimeStub(initialState = {}) {
|
|
7
|
+
let state = Object.freeze({
|
|
8
|
+
authenticated: Boolean(initialState.authenticated),
|
|
9
|
+
username: String(initialState.username || ""),
|
|
10
|
+
oauthDefaultProvider: String(initialState.oauthDefaultProvider || ""),
|
|
11
|
+
oauthProviders: Array.isArray(initialState.oauthProviders)
|
|
12
|
+
? Object.freeze([...initialState.oauthProviders])
|
|
13
|
+
: Object.freeze([])
|
|
14
|
+
});
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
async initialize() {
|
|
19
|
+
return state;
|
|
20
|
+
},
|
|
21
|
+
async refresh() {
|
|
22
|
+
return state;
|
|
23
|
+
},
|
|
24
|
+
getState() {
|
|
25
|
+
return state;
|
|
26
|
+
},
|
|
27
|
+
subscribe(listener) {
|
|
28
|
+
if (typeof listener === "function") {
|
|
29
|
+
listeners.add(listener);
|
|
30
|
+
}
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(listener);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
push(nextState = {}) {
|
|
36
|
+
state = Object.freeze({
|
|
37
|
+
authenticated: Boolean(nextState.authenticated),
|
|
38
|
+
username: String(nextState.username || ""),
|
|
39
|
+
oauthDefaultProvider: String(nextState.oauthDefaultProvider || ""),
|
|
40
|
+
oauthProviders: Array.isArray(nextState.oauthProviders)
|
|
41
|
+
? Object.freeze([...nextState.oauthProviders])
|
|
42
|
+
: Object.freeze([])
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
for (const listener of listeners) {
|
|
46
|
+
listener(state);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("auth store exposes reactive state and direct runtime methods", async () => {
|
|
53
|
+
const pinia = createPinia();
|
|
54
|
+
const runtime = createAuthRuntimeStub({
|
|
55
|
+
authenticated: false,
|
|
56
|
+
username: ""
|
|
57
|
+
});
|
|
58
|
+
const auth = useAuthStore(pinia);
|
|
59
|
+
|
|
60
|
+
auth.attachRuntime(runtime);
|
|
61
|
+
|
|
62
|
+
assert.equal(auth.runtime, runtime);
|
|
63
|
+
assert.equal(auth.authenticated, false);
|
|
64
|
+
assert.equal(auth.username, "");
|
|
65
|
+
assert.deepEqual(auth.oauthProviders, []);
|
|
66
|
+
assert.equal(auth.oauthDefaultProvider, "");
|
|
67
|
+
assert.equal(auth.getState().authenticated, false);
|
|
68
|
+
assert.equal(await auth.refresh(), runtime.getState());
|
|
69
|
+
|
|
70
|
+
runtime.push({
|
|
71
|
+
authenticated: true,
|
|
72
|
+
username: "ada",
|
|
73
|
+
oauthProviders: [{ id: "google", label: "Google" }],
|
|
74
|
+
oauthDefaultProvider: "google"
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.equal(auth.authenticated, true);
|
|
78
|
+
assert.equal(auth.username, "ada");
|
|
79
|
+
assert.deepEqual(auth.oauthProviders, [{ id: "google", label: "Google" }]);
|
|
80
|
+
assert.equal(auth.oauthDefaultProvider, "google");
|
|
81
|
+
assert.equal(auth.getState().authenticated, true);
|
|
82
|
+
|
|
83
|
+
let observedState = null;
|
|
84
|
+
const unsubscribe = auth.subscribe((nextState) => {
|
|
85
|
+
observedState = nextState;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
runtime.push({
|
|
89
|
+
authenticated: true,
|
|
90
|
+
username: "grace"
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
assert.equal(observedState?.username, "grace");
|
|
94
|
+
unsubscribe();
|
|
95
|
+
});
|