@structured-id/ui-core 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +47 -0
- package/dist/browser-vp/index.cjs +349 -0
- package/dist/browser-vp/index.d.cts +287 -0
- package/dist/browser-vp/index.d.ts +287 -0
- package/dist/browser-vp/index.js +349 -0
- package/dist/chunk-C3Q766CY.cjs +125 -0
- package/dist/chunk-J5YXRXB5.js +125 -0
- package/dist/index.cjs +19360 -0
- package/dist/index.d.cts +9536 -0
- package/dist/index.d.ts +9536 -0
- package/dist/index.js +19360 -0
- package/dist/storage-4CB2JTLW.cjs +22 -0
- package/dist/storage-THRWREZP.js +22 -0
- package/package.json +93 -0
- package/src/browser-vp/crypto.ts +254 -0
- package/src/browser-vp/index.test.ts +10 -0
- package/src/browser-vp/index.ts +118 -0
- package/src/browser-vp/prf.ts +181 -0
- package/src/browser-vp/storage.test.ts +9 -0
- package/src/browser-vp/storage.ts +165 -0
- package/src/browser-vp/types.ts +97 -0
- package/src/browser-vp/vp.test.ts +43 -0
- package/src/browser-vp/vp.ts +113 -0
- package/src/components/SidAvatar.vue +106 -0
- package/src/components/SidConfirmDialog.vue +125 -0
- package/src/components/SidStatusBadge.vue +71 -0
- package/src/components/index.ts +5 -0
- package/src/composables/index.ts +10 -0
- package/src/composables/useAuth.ts +40 -0
- package/src/composables/useBffAuth.ts +147 -0
- package/src/composables/useGrpc.test.ts +196 -0
- package/src/composables/useGrpc.ts +120 -0
- package/src/composables/useSession.ts +80 -0
- package/src/generated/google/api/http.ts +687 -0
- package/src/generated/google/protobuf/descriptor.ts +4865 -0
- package/src/generated/google/protobuf/duration.ts +230 -0
- package/src/generated/google/protobuf/empty.ts +94 -0
- package/src/generated/google/protobuf/struct.ts +484 -0
- package/src/generated/google/protobuf/timestamp.ts +287 -0
- package/src/generated/sid/v1/account/account.client.ts +527 -0
- package/src/generated/sid/v1/account/account.ts +3261 -0
- package/src/generated/sid/v1/admin/admin.client.ts +136 -0
- package/src/generated/sid/v1/admin/admin.ts +846 -0
- package/src/generated/sid/v1/admin/admin_roles.client.ts +181 -0
- package/src/generated/sid/v1/admin/admin_roles.ts +1606 -0
- package/src/generated/sid/v1/admin/analytics.client.ts +106 -0
- package/src/generated/sid/v1/admin/analytics.ts +1017 -0
- package/src/generated/sid/v1/admin/branding.client.ts +166 -0
- package/src/generated/sid/v1/admin/branding.ts +1906 -0
- package/src/generated/sid/v1/admin/enrollment.client.ts +165 -0
- package/src/generated/sid/v1/admin/enrollment.ts +1704 -0
- package/src/generated/sid/v1/admin/events.client.ts +195 -0
- package/src/generated/sid/v1/admin/events.ts +1900 -0
- package/src/generated/sid/v1/admin/flow.client.ts +183 -0
- package/src/generated/sid/v1/admin/flow.ts +1552 -0
- package/src/generated/sid/v1/admin/keys.client.ts +107 -0
- package/src/generated/sid/v1/admin/keys.ts +931 -0
- package/src/generated/sid/v1/admin/login_experience.client.ts +160 -0
- package/src/generated/sid/v1/admin/login_experience.ts +1061 -0
- package/src/generated/sid/v1/admin/migration.client.ts +127 -0
- package/src/generated/sid/v1/admin/migration.ts +1948 -0
- package/src/generated/sid/v1/admin/notifications.client.ts +128 -0
- package/src/generated/sid/v1/admin/notifications.ts +1332 -0
- package/src/generated/sid/v1/admin/operations.client.ts +241 -0
- package/src/generated/sid/v1/admin/operations.ts +2648 -0
- package/src/generated/sid/v1/admin/organizations.client.ts +57 -0
- package/src/generated/sid/v1/admin/organizations.ts +349 -0
- package/src/generated/sid/v1/admin/scopes.client.ts +171 -0
- package/src/generated/sid/v1/admin/scopes.ts +1324 -0
- package/src/generated/sid/v1/admin/security.client.ts +412 -0
- package/src/generated/sid/v1/admin/security.ts +4086 -0
- package/src/generated/sid/v1/admin/sessions.client.ts +114 -0
- package/src/generated/sid/v1/admin/sessions.ts +923 -0
- package/src/generated/sid/v1/admin/settings.client.ts +340 -0
- package/src/generated/sid/v1/admin/settings.ts +2395 -0
- package/src/generated/sid/v1/admin/user_profile.client.ts +57 -0
- package/src/generated/sid/v1/admin/user_profile.ts +730 -0
- package/src/generated/sid/v1/admin/webhooks.client.ts +269 -0
- package/src/generated/sid/v1/admin/webhooks.ts +2804 -0
- package/src/generated/sid/v1/attestation/attestation.client.ts +115 -0
- package/src/generated/sid/v1/attestation/attestation.ts +813 -0
- package/src/generated/sid/v1/authn/auth.client.ts +900 -0
- package/src/generated/sid/v1/authn/auth.ts +9287 -0
- package/src/generated/sid/v1/authn/flows.client.ts +413 -0
- package/src/generated/sid/v1/authn/flows.ts +3217 -0
- package/src/generated/sid/v1/authz/authz.client.ts +459 -0
- package/src/generated/sid/v1/authz/authz.ts +5343 -0
- package/src/generated/sid/v1/authz/governance.client.ts +102 -0
- package/src/generated/sid/v1/authz/governance.ts +818 -0
- package/src/generated/sid/v1/common/errors.ts +184 -0
- package/src/generated/sid/v1/common/pagination.ts +152 -0
- package/src/generated/sid/v1/common/transport.ts +227 -0
- package/src/generated/sid/v1/common/types.ts +151 -0
- package/src/generated/sid/v1/events/event_stream.client.ts +67 -0
- package/src/generated/sid/v1/events/event_stream.ts +374 -0
- package/src/generated/sid/v1/federation/upstream.client.ts +142 -0
- package/src/generated/sid/v1/federation/upstream.ts +1736 -0
- package/src/generated/sid/v1/identity/identity.client.ts +467 -0
- package/src/generated/sid/v1/identity/identity.ts +5613 -0
- package/src/generated/sid/v1/machine/machine_user.client.ts +219 -0
- package/src/generated/sid/v1/machine/machine_user.ts +2400 -0
- package/src/generated/sid/v1/machine/pat.client.ts +147 -0
- package/src/generated/sid/v1/machine/pat.ts +1268 -0
- package/src/generated/sid/v1/projects/projects.client.ts +286 -0
- package/src/generated/sid/v1/projects/projects.ts +2714 -0
- package/src/generated/sid/v1/scim/scim.client.ts +274 -0
- package/src/generated/sid/v1/scim/scim.ts +3043 -0
- package/src/generated/sid/v1/test/test.client.ts +232 -0
- package/src/generated/sid/v1/test/test.ts +1136 -0
- package/src/i18n/index.ts +40 -0
- package/src/index.ts +157 -0
- package/src/profile/auth.ts +15 -0
- package/src/profile/components/ConsentManager.vue +88 -0
- package/src/profile/components/ProfileEditor.vue +146 -0
- package/src/profile/components/SecuritySettings.vue +170 -0
- package/src/profile/components/SessionList.vue +100 -0
- package/src/profile/components/SidLoginForm.test.ts +80 -0
- package/src/profile/components/SidLoginForm.vue +230 -0
- package/src/profile/components/SidMfaChallenge.test.ts +127 -0
- package/src/profile/components/SidMfaChallenge.vue +243 -0
- package/src/profile/components/SidRegistrationForm.test.ts +79 -0
- package/src/profile/components/SidRegistrationForm.vue +317 -0
- package/src/profile/components/index.ts +11 -0
- package/src/profile/composables/index.ts +6 -0
- package/src/profile/composables/useAccountApi.ts +203 -0
- package/src/profile/composables/useAuthApi.opaque.test.ts +140 -0
- package/src/profile/composables/useAuthApi.ts +617 -0
- package/src/profile/composables/useIdentityApi.ts +123 -0
- package/src/profile/composables/useLoginSuccess.test.ts +112 -0
- package/src/profile/composables/useLoginSuccess.ts +65 -0
- package/src/profile/composables/useOpaqueAuth.test.ts +290 -0
- package/src/profile/composables/useOpaqueAuth.ts +191 -0
- package/src/profile/composables/useSecurityLevel.test.ts +198 -0
- package/src/profile/composables/useSecurityLevel.ts +324 -0
- package/src/profile/index.ts +18 -0
- package/src/quasar/SidPrincipalInput.vue +209 -0
- package/src/quasar/index.ts +8 -0
- package/src/stores/auth.test.ts +83 -0
- package/src/stores/auth.ts +92 -0
- package/src/stores/index.ts +1 -0
- package/src/transport/adaptive.test.ts +728 -0
- package/src/transport/adaptive.ts +308 -0
- package/src/transport/framing.test.ts +227 -0
- package/src/transport/framing.ts +98 -0
- package/src/transport/webtransport.test.ts +728 -0
- package/src/transport/webtransport.ts +323 -0
- package/src/types/auth.ts +58 -0
- package/src/types/index.ts +1 -0
- package/src/utils/formatters.test.ts +26 -0
- package/src/utils/formatters.ts +69 -0
- package/src/utils/index.ts +16 -0
- package/src/utils/normalizePrincipal.test.ts +188 -0
- package/src/utils/normalizePrincipal.ts +159 -0
- package/src/utils/validators.test.ts +75 -0
- package/src/utils/validators.ts +36 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* profile-core — shared Profile-level UI library.
|
|
3
|
+
*
|
|
4
|
+
* Composables for identity, account, auth, and security APIs.
|
|
5
|
+
* Slot-based form components for registration, login, MFA.
|
|
6
|
+
* Shared between profile-ui-ce, profile-ui-ee, and frontend.
|
|
7
|
+
*
|
|
8
|
+
* Import: `@structured-id/ui-core/profile`
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Composables
|
|
12
|
+
export * from "./composables";
|
|
13
|
+
|
|
14
|
+
// Slot-based form components
|
|
15
|
+
export * from "./components";
|
|
16
|
+
|
|
17
|
+
// Auth helper
|
|
18
|
+
export { authMeta } from "./auth";
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-input
|
|
3
|
+
ref="inputRef"
|
|
4
|
+
v-model="model"
|
|
5
|
+
:label="label"
|
|
6
|
+
:rules="[requiredRule, typeRule]"
|
|
7
|
+
outlined
|
|
8
|
+
type="text"
|
|
9
|
+
:inputmode="inputMode"
|
|
10
|
+
autocapitalize="none"
|
|
11
|
+
autocomplete="username webauthn"
|
|
12
|
+
spellcheck="false"
|
|
13
|
+
:lazy-rules="lazyRules"
|
|
14
|
+
@update:model-value="onInput"
|
|
15
|
+
>
|
|
16
|
+
<template #prepend>
|
|
17
|
+
<q-icon :name="iconName" :color="iconColor" size="24px" />
|
|
18
|
+
</template>
|
|
19
|
+
<template v-if="model && detectedType !== 'unknown'" #append>
|
|
20
|
+
<q-chip
|
|
21
|
+
dense
|
|
22
|
+
:color="chipColor"
|
|
23
|
+
text-color="white"
|
|
24
|
+
size="sm"
|
|
25
|
+
class="q-px-sm"
|
|
26
|
+
>
|
|
27
|
+
{{ chipLabel }}
|
|
28
|
+
</q-chip>
|
|
29
|
+
</template>
|
|
30
|
+
</q-input>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { ref, computed, watch } from "vue";
|
|
35
|
+
import { isValidPhoneNumber } from "libphonenumber-js";
|
|
36
|
+
|
|
37
|
+
export type PrincipalType = "email" | "phone" | "username" | "unknown";
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
modelValue: string;
|
|
41
|
+
minUsernameLength?: number;
|
|
42
|
+
required?: boolean;
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Validate on each input change (false) vs only on blur (true). Default false — eager. */
|
|
45
|
+
lazyRules?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
49
|
+
modelValue: "",
|
|
50
|
+
// Per arch/identity/identity-model.md §Principal: username regex is
|
|
51
|
+
// ^[a-z0-9_]{3,32}$ (display form ^[a-zA-Z0-9_]{3,32}$).
|
|
52
|
+
minUsernameLength: 3,
|
|
53
|
+
required: true,
|
|
54
|
+
label: "Email, phone, or username",
|
|
55
|
+
lazyRules: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const emit = defineEmits<{
|
|
59
|
+
"update:modelValue": [value: string];
|
|
60
|
+
"update:principalType": [type: PrincipalType];
|
|
61
|
+
}>();
|
|
62
|
+
|
|
63
|
+
const inputRef = ref<{ focus: () => void } | null>(null);
|
|
64
|
+
|
|
65
|
+
const model = computed({
|
|
66
|
+
get: () => props.modelValue,
|
|
67
|
+
set: (v) => emit("update:modelValue", v),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const USERNAME_MAX_LENGTH = 32;
|
|
71
|
+
|
|
72
|
+
function detectType(value: string): PrincipalType {
|
|
73
|
+
if (!value) return "unknown";
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed) return "unknown";
|
|
76
|
+
|
|
77
|
+
// Phone: starts with +
|
|
78
|
+
if (trimmed.startsWith("+")) return "phone";
|
|
79
|
+
|
|
80
|
+
// Email: contains @ (checked after phone to exclude edge cases)
|
|
81
|
+
if (trimmed.includes("@")) return "email";
|
|
82
|
+
|
|
83
|
+
// Username per arch/identity/identity-model.md:
|
|
84
|
+
// ^[a-z0-9_]{3,32}$, no leading/trailing _, no __
|
|
85
|
+
// Detection is relaxed: accepts uppercase (will normalize), checks min length
|
|
86
|
+
const re = new RegExp(
|
|
87
|
+
`^[a-zA-Z0-9_]{${props.minUsernameLength},${USERNAME_MAX_LENGTH}}$`,
|
|
88
|
+
);
|
|
89
|
+
if (re.test(trimmed) && !trimmed.startsWith("_") && !trimmed.endsWith("_")) {
|
|
90
|
+
return "username";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Still typing — not enough chars to determine
|
|
94
|
+
return "unknown";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const detectedType = computed(() => detectType(model.value));
|
|
98
|
+
|
|
99
|
+
// Emit type changes
|
|
100
|
+
watch(detectedType, (type) => {
|
|
101
|
+
emit("update:principalType", type);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function onInput(value: string | number | null) {
|
|
105
|
+
emit("update:principalType", detectType(String(value ?? "")));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Validation rules ---
|
|
109
|
+
|
|
110
|
+
const requiredRule = (val: string) =>
|
|
111
|
+
!props.required || !!val || "This field is required";
|
|
112
|
+
|
|
113
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
114
|
+
|
|
115
|
+
const typeRule = (val: string): string | true => {
|
|
116
|
+
if (!val) return true; // required rule handles empty
|
|
117
|
+
const type = detectType(val);
|
|
118
|
+
|
|
119
|
+
switch (type) {
|
|
120
|
+
case "email":
|
|
121
|
+
return emailRegex.test(val) || "Invalid email address";
|
|
122
|
+
case "phone":
|
|
123
|
+
return isValidPhoneNumber(val) || "Invalid phone number";
|
|
124
|
+
case "username": {
|
|
125
|
+
const lower = val.toLowerCase();
|
|
126
|
+
const re = new RegExp(
|
|
127
|
+
`^[a-z0-9_]{${props.minUsernameLength},${USERNAME_MAX_LENGTH}}$`,
|
|
128
|
+
);
|
|
129
|
+
if (!re.test(lower)) return "Invalid username";
|
|
130
|
+
if (lower.startsWith("_") || lower.endsWith("_"))
|
|
131
|
+
return "Username cannot start or end with underscore";
|
|
132
|
+
if (lower.includes("__"))
|
|
133
|
+
return "Username cannot contain consecutive underscores";
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
default:
|
|
137
|
+
return "Enter email, phone (+...), or username";
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// --- Dynamic icon & type ---
|
|
142
|
+
|
|
143
|
+
const iconName = computed(() => {
|
|
144
|
+
switch (detectedType.value) {
|
|
145
|
+
case "email":
|
|
146
|
+
return "sym_o_mail";
|
|
147
|
+
case "phone":
|
|
148
|
+
return "sym_o_phone";
|
|
149
|
+
case "username":
|
|
150
|
+
return "sym_o_person";
|
|
151
|
+
default:
|
|
152
|
+
return "sym_o_login";
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const iconColor = computed(() =>
|
|
157
|
+
detectedType.value === "unknown" ? "grey-6" : "primary",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// inputmode controls mobile keyboard WITHOUT triggering browser autofill.
|
|
161
|
+
// type="text" stays constant — changing type mid-input causes:
|
|
162
|
+
// - autofill popup flashing (browser re-evaluates field purpose)
|
|
163
|
+
// - cursor/value reset in some browsers
|
|
164
|
+
// - mobile keyboard switching mid-keystroke
|
|
165
|
+
const inputMode = computed(() => {
|
|
166
|
+
switch (detectedType.value) {
|
|
167
|
+
case "email":
|
|
168
|
+
return "email"; // keyboard with @ key
|
|
169
|
+
case "phone":
|
|
170
|
+
return "tel"; // numeric keyboard
|
|
171
|
+
default:
|
|
172
|
+
return "text";
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const chipLabel = computed(() => {
|
|
177
|
+
switch (detectedType.value) {
|
|
178
|
+
case "email":
|
|
179
|
+
return "email";
|
|
180
|
+
case "phone":
|
|
181
|
+
return "phone";
|
|
182
|
+
case "username":
|
|
183
|
+
return "username";
|
|
184
|
+
default:
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const chipColor = computed(() => {
|
|
190
|
+
switch (detectedType.value) {
|
|
191
|
+
case "email":
|
|
192
|
+
return "deep-orange";
|
|
193
|
+
case "phone":
|
|
194
|
+
return "green";
|
|
195
|
+
case "username":
|
|
196
|
+
return "blue";
|
|
197
|
+
default:
|
|
198
|
+
return "grey";
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// --- Public API ---
|
|
203
|
+
|
|
204
|
+
function focus() {
|
|
205
|
+
inputRef.value?.focus();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
defineExpose({ focus, detectedType });
|
|
209
|
+
</script>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quasar-dependent components.
|
|
3
|
+
*
|
|
4
|
+
* Separate entrypoint from main ui-core (which is pure Vue 3 + TS).
|
|
5
|
+
* Import as: import { SidPrincipalInput } from '@structured-id/ui-core/quasar'
|
|
6
|
+
*/
|
|
7
|
+
export { default as SidPrincipalInput } from "./SidPrincipalInput.vue";
|
|
8
|
+
export type { PrincipalType } from "./SidPrincipalInput.vue";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { setActivePinia, createPinia } from "pinia";
|
|
3
|
+
import { useAuthStore } from "./auth";
|
|
4
|
+
|
|
5
|
+
describe("useAuthStore (BFF cookie session)", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
setActivePinia(createPinia());
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("starts unauthenticated with empty state", () => {
|
|
11
|
+
const store = useAuthStore();
|
|
12
|
+
expect(store.isAuthenticated).toBe(false);
|
|
13
|
+
expect(store.sessionValid).toBe(false);
|
|
14
|
+
expect(store.profileId).toBeNull();
|
|
15
|
+
expect(store.displayName).toBeNull();
|
|
16
|
+
expect(store.email).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("setBffSession sets sessionValid and profile data", () => {
|
|
20
|
+
const store = useAuthStore();
|
|
21
|
+
store.setBffSession({
|
|
22
|
+
sub: "profile-uuid-123",
|
|
23
|
+
email: "alice@example.com",
|
|
24
|
+
name: "Alice Smith",
|
|
25
|
+
preferred_username: "alice",
|
|
26
|
+
groups: ["admins"],
|
|
27
|
+
acr: "urn:sid:acr:mfa",
|
|
28
|
+
roles: ["member"],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(store.isAuthenticated).toBe(true);
|
|
32
|
+
expect(store.sessionValid).toBe(true);
|
|
33
|
+
expect(store.profileId).toBe("profile-uuid-123");
|
|
34
|
+
expect(store.displayName).toBe("Alice Smith");
|
|
35
|
+
expect(store.email).toBe("alice@example.com");
|
|
36
|
+
expect(store.preferredUsername).toBe("alice");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("setBffSession handles null/missing optional fields", () => {
|
|
40
|
+
const store = useAuthStore();
|
|
41
|
+
store.setBffSession({ sub: "uuid-456" });
|
|
42
|
+
|
|
43
|
+
expect(store.isAuthenticated).toBe(true);
|
|
44
|
+
expect(store.profileId).toBe("uuid-456");
|
|
45
|
+
expect(store.displayName).toBeNull();
|
|
46
|
+
expect(store.email).toBeNull();
|
|
47
|
+
expect(store.preferredUsername).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("setProfile updates display data", () => {
|
|
51
|
+
const store = useAuthStore();
|
|
52
|
+
store.setBffSession({ sub: "uuid-1" });
|
|
53
|
+
|
|
54
|
+
store.setProfile({
|
|
55
|
+
id: "uuid-1",
|
|
56
|
+
displayName: "Bob Jones",
|
|
57
|
+
email: "bob@example.com",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(store.profileId).toBe("uuid-1");
|
|
61
|
+
expect(store.displayName).toBe("Bob Jones");
|
|
62
|
+
expect(store.email).toBe("bob@example.com");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("logout clears all state", () => {
|
|
66
|
+
const store = useAuthStore();
|
|
67
|
+
store.setBffSession({
|
|
68
|
+
sub: "uuid-1",
|
|
69
|
+
name: "Alice",
|
|
70
|
+
email: "alice@example.com",
|
|
71
|
+
preferred_username: "alice",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
store.logout();
|
|
75
|
+
|
|
76
|
+
expect(store.isAuthenticated).toBe(false);
|
|
77
|
+
expect(store.sessionValid).toBe(false);
|
|
78
|
+
expect(store.profileId).toBeNull();
|
|
79
|
+
expect(store.displayName).toBeNull();
|
|
80
|
+
expect(store.email).toBeNull();
|
|
81
|
+
expect(store.preferredUsername).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineStore } from "pinia";
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import type { BffUserInfo } from "../composables/useBffAuth";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auth store — BFF cookie session (post #536i-3 migration).
|
|
7
|
+
*
|
|
8
|
+
* Tokens are stored server-side in sid-auth-proxy BFF sessions.
|
|
9
|
+
* The SPA never sees access/refresh tokens — only an httpOnly session cookie.
|
|
10
|
+
* `isAuthenticated` is based on BFF session validity, set via `setBffSession()`.
|
|
11
|
+
*
|
|
12
|
+
* Boot sequence:
|
|
13
|
+
* 1. App boots → calls checkBffSession() → populates store via setBffSession()
|
|
14
|
+
* 2. Route guard → checks isAuthenticated → redirects to BFF login if false
|
|
15
|
+
* 3. On BFF logout → call logout() to clear local state
|
|
16
|
+
*/
|
|
17
|
+
export const useAuthStore = defineStore("auth", () => {
|
|
18
|
+
// ── BFF session state ─────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Whether a valid BFF session exists (set by setBffSession on boot). */
|
|
21
|
+
const sessionValid = ref(false);
|
|
22
|
+
|
|
23
|
+
/** Server-side session identifier (for step-up, session management). */
|
|
24
|
+
const sessionId = ref<string | null>(null);
|
|
25
|
+
|
|
26
|
+
// ── Profile info (populated from BFF /auth/userinfo) ──────────────
|
|
27
|
+
|
|
28
|
+
/** Profile ID of the authenticated user (= sub from BFF userinfo). */
|
|
29
|
+
const profileId = ref<string | null>(null);
|
|
30
|
+
/** Display name. */
|
|
31
|
+
const displayName = ref<string | null>(null);
|
|
32
|
+
/** Email address. */
|
|
33
|
+
const email = ref<string | null>(null);
|
|
34
|
+
/** Username or email — preferred display identifier. */
|
|
35
|
+
const preferredUsername = ref<string | null>(null);
|
|
36
|
+
|
|
37
|
+
// ── Computed ─────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const isAuthenticated = computed(() => sessionValid.value);
|
|
40
|
+
|
|
41
|
+
// ── Actions ──────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Populate store from BFF session.
|
|
45
|
+
* Called after successful checkBffSession() on app boot or after login redirect.
|
|
46
|
+
*/
|
|
47
|
+
function setBffSession(info: BffUserInfo): void {
|
|
48
|
+
sessionValid.value = true;
|
|
49
|
+
profileId.value = info.sub;
|
|
50
|
+
displayName.value = info.name ?? null;
|
|
51
|
+
email.value = info.email ?? null;
|
|
52
|
+
preferredUsername.value = info.preferred_username ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update profile display data (e.g., after profile edit).
|
|
57
|
+
*/
|
|
58
|
+
function setProfile(profile: {
|
|
59
|
+
id: string;
|
|
60
|
+
displayName?: string | null;
|
|
61
|
+
email?: string | null;
|
|
62
|
+
}): void {
|
|
63
|
+
profileId.value = profile.id;
|
|
64
|
+
displayName.value = profile.displayName ?? null;
|
|
65
|
+
email.value = profile.email ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Clear all session state. Call after bffLogout() completes.
|
|
70
|
+
*/
|
|
71
|
+
function logout(): void {
|
|
72
|
+
sessionValid.value = false;
|
|
73
|
+
sessionId.value = null;
|
|
74
|
+
profileId.value = null;
|
|
75
|
+
displayName.value = null;
|
|
76
|
+
email.value = null;
|
|
77
|
+
preferredUsername.value = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
sessionValid,
|
|
82
|
+
sessionId,
|
|
83
|
+
profileId,
|
|
84
|
+
displayName,
|
|
85
|
+
email,
|
|
86
|
+
preferredUsername,
|
|
87
|
+
isAuthenticated,
|
|
88
|
+
setBffSession,
|
|
89
|
+
setProfile,
|
|
90
|
+
logout,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useAuthStore } from "./auth";
|