@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,243 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-card flat bordered>
|
|
3
|
+
<q-card-section>
|
|
4
|
+
<slot name="header">
|
|
5
|
+
<div class="text-h6">{{ title }}</div>
|
|
6
|
+
<div class="text-caption text-grey">{{ subtitle }}</div>
|
|
7
|
+
</slot>
|
|
8
|
+
</q-card-section>
|
|
9
|
+
|
|
10
|
+
<q-separator />
|
|
11
|
+
|
|
12
|
+
<q-card-section>
|
|
13
|
+
<q-banner
|
|
14
|
+
v-if="error"
|
|
15
|
+
class="bg-negative text-white q-mb-md"
|
|
16
|
+
rounded
|
|
17
|
+
dense
|
|
18
|
+
>
|
|
19
|
+
{{ error }}
|
|
20
|
+
</q-banner>
|
|
21
|
+
|
|
22
|
+
<!-- Method selection -->
|
|
23
|
+
<template v-if="phase === 'select'">
|
|
24
|
+
<div class="text-center q-mb-md">
|
|
25
|
+
<q-icon name="sym_o_security" size="48px" color="warning" />
|
|
26
|
+
<div class="text-body2 text-grey-7 q-mt-sm">
|
|
27
|
+
{{ reason || "You need to verify your identity before continuing" }}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="text-subtitle2 q-mb-sm">Verify using:</div>
|
|
32
|
+
|
|
33
|
+
<q-list separator>
|
|
34
|
+
<q-item
|
|
35
|
+
v-for="method in methods"
|
|
36
|
+
:key="method.type"
|
|
37
|
+
clickable
|
|
38
|
+
v-ripple
|
|
39
|
+
@click="selectMethod(method.type)"
|
|
40
|
+
>
|
|
41
|
+
<q-item-section avatar>
|
|
42
|
+
<q-icon :name="method.icon" :color="method.color" />
|
|
43
|
+
</q-item-section>
|
|
44
|
+
<q-item-section>
|
|
45
|
+
<q-item-label>{{ method.label }}</q-item-label>
|
|
46
|
+
<q-item-label caption>{{ method.description }}</q-item-label>
|
|
47
|
+
</q-item-section>
|
|
48
|
+
<q-item-section side>
|
|
49
|
+
<q-badge
|
|
50
|
+
v-if="method.recommended"
|
|
51
|
+
color="positive"
|
|
52
|
+
label="Recommended"
|
|
53
|
+
/>
|
|
54
|
+
<q-icon v-else name="sym_o_chevron_right" />
|
|
55
|
+
</q-item-section>
|
|
56
|
+
</q-item>
|
|
57
|
+
</q-list>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<!-- TOTP code entry -->
|
|
61
|
+
<template v-if="phase === 'totp'">
|
|
62
|
+
<q-form @submit.prevent="submitCode('totp')" class="q-gutter-md">
|
|
63
|
+
<q-input
|
|
64
|
+
v-model="totpCode"
|
|
65
|
+
label="Authentication code"
|
|
66
|
+
outlined
|
|
67
|
+
autofocus
|
|
68
|
+
mask="### ###"
|
|
69
|
+
unmasked-value
|
|
70
|
+
:disable="loading"
|
|
71
|
+
:rules="[
|
|
72
|
+
(v: string) => (!!v && v.length === 6) || 'Enter 6-digit code',
|
|
73
|
+
]"
|
|
74
|
+
input-class="text-center text-h5 letter-spacing-wide"
|
|
75
|
+
inputmode="numeric"
|
|
76
|
+
autocomplete="one-time-code"
|
|
77
|
+
>
|
|
78
|
+
<template v-slot:prepend>
|
|
79
|
+
<q-icon name="sym_o_timer" color="primary" />
|
|
80
|
+
</template>
|
|
81
|
+
</q-input>
|
|
82
|
+
|
|
83
|
+
<q-btn
|
|
84
|
+
type="submit"
|
|
85
|
+
label="Verify"
|
|
86
|
+
color="primary"
|
|
87
|
+
class="full-width"
|
|
88
|
+
:loading="loading"
|
|
89
|
+
:disable="!totpCode || totpCode.length < 6"
|
|
90
|
+
/>
|
|
91
|
+
</q-form>
|
|
92
|
+
|
|
93
|
+
<div class="text-caption text-grey-6 text-center q-mt-md">
|
|
94
|
+
<q-icon name="sym_o_info" size="xs" class="q-mr-xs" />
|
|
95
|
+
Open your authenticator app to find the code
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<!-- Recovery code entry -->
|
|
100
|
+
<template v-if="phase === 'recovery'">
|
|
101
|
+
<q-form @submit.prevent="submitCode('recovery')" class="q-gutter-md">
|
|
102
|
+
<q-input
|
|
103
|
+
v-model="recoveryCode"
|
|
104
|
+
label="Recovery code"
|
|
105
|
+
outlined
|
|
106
|
+
autofocus
|
|
107
|
+
:disable="loading"
|
|
108
|
+
:rules="[(v: string) => !!v || 'Enter recovery code']"
|
|
109
|
+
input-class="text-center text-h6 font-monospace"
|
|
110
|
+
>
|
|
111
|
+
<template v-slot:prepend>
|
|
112
|
+
<q-icon name="sym_o_key" color="warning" />
|
|
113
|
+
</template>
|
|
114
|
+
</q-input>
|
|
115
|
+
|
|
116
|
+
<q-btn
|
|
117
|
+
type="submit"
|
|
118
|
+
label="Verify"
|
|
119
|
+
color="primary"
|
|
120
|
+
class="full-width"
|
|
121
|
+
:loading="loading"
|
|
122
|
+
:disable="!recoveryCode"
|
|
123
|
+
/>
|
|
124
|
+
</q-form>
|
|
125
|
+
|
|
126
|
+
<div class="text-caption text-grey-6 text-center q-mt-md">
|
|
127
|
+
<q-icon name="sym_o_warning" size="xs" class="q-mr-xs" />
|
|
128
|
+
Recovery codes are single-use
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<!-- WebAuthn (passkey) -->
|
|
133
|
+
<template v-if="phase === 'webauthn'">
|
|
134
|
+
<div class="text-center q-pa-lg">
|
|
135
|
+
<q-spinner-dots v-if="loading" size="48px" color="primary" />
|
|
136
|
+
<q-icon v-else name="sym_o_passkey" size="48px" color="primary" />
|
|
137
|
+
<div class="text-body2 text-grey-7 q-mt-md">
|
|
138
|
+
{{
|
|
139
|
+
loading
|
|
140
|
+
? "Waiting for authenticator\u2026"
|
|
141
|
+
: "Use your passkey or security key to verify"
|
|
142
|
+
}}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
|
|
147
|
+
<!-- Success -->
|
|
148
|
+
<template v-if="phase === 'success'">
|
|
149
|
+
<div class="text-center q-pa-lg">
|
|
150
|
+
<q-icon name="sym_o_check_circle" size="48px" color="positive" />
|
|
151
|
+
<div class="text-body1 text-positive q-mt-sm">Verified</div>
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
154
|
+
|
|
155
|
+
<slot name="below-challenge" :phase="phase" />
|
|
156
|
+
</q-card-section>
|
|
157
|
+
|
|
158
|
+
<slot name="footer" />
|
|
159
|
+
</q-card>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<script setup lang="ts">
|
|
163
|
+
import { ref } from "vue";
|
|
164
|
+
|
|
165
|
+
export interface MfaMethod {
|
|
166
|
+
type: "totp" | "webauthn" | "recovery";
|
|
167
|
+
icon: string;
|
|
168
|
+
label: string;
|
|
169
|
+
description: string;
|
|
170
|
+
color: string;
|
|
171
|
+
recommended?: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type Phase = "select" | "totp" | "webauthn" | "recovery" | "success";
|
|
175
|
+
|
|
176
|
+
withDefaults(
|
|
177
|
+
defineProps<{
|
|
178
|
+
title?: string;
|
|
179
|
+
subtitle?: string;
|
|
180
|
+
reason?: string;
|
|
181
|
+
methods: MfaMethod[];
|
|
182
|
+
}>(),
|
|
183
|
+
{
|
|
184
|
+
title: "Additional verification required",
|
|
185
|
+
subtitle: "This action requires a higher authentication level",
|
|
186
|
+
reason: "",
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const emit = defineEmits<{
|
|
191
|
+
"select-method": [method: string];
|
|
192
|
+
"submit-totp": [code: string];
|
|
193
|
+
"submit-recovery": [code: string];
|
|
194
|
+
"submit-webauthn": [];
|
|
195
|
+
verified: [];
|
|
196
|
+
}>();
|
|
197
|
+
|
|
198
|
+
const phase = ref<Phase>("select");
|
|
199
|
+
const loading = ref(false);
|
|
200
|
+
const error = ref<string | null>(null);
|
|
201
|
+
const totpCode = ref("");
|
|
202
|
+
const recoveryCode = ref("");
|
|
203
|
+
|
|
204
|
+
function selectMethod(method: string) {
|
|
205
|
+
error.value = null;
|
|
206
|
+
phase.value = method as Phase;
|
|
207
|
+
emit("select-method", method);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function submitCode(method: "totp" | "recovery") {
|
|
211
|
+
if (method === "totp") {
|
|
212
|
+
emit("submit-totp", totpCode.value);
|
|
213
|
+
} else {
|
|
214
|
+
emit("submit-recovery", recoveryCode.value);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Called by parent to set loading state */
|
|
219
|
+
function setLoading(val: boolean) {
|
|
220
|
+
loading.value = val;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Called by parent to set error */
|
|
224
|
+
function setError(msg: string | null) {
|
|
225
|
+
error.value = msg;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Called by parent to show success */
|
|
229
|
+
function setSuccess() {
|
|
230
|
+
phase.value = "success";
|
|
231
|
+
emit("verified");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Called by parent to reset to method selection */
|
|
235
|
+
function reset() {
|
|
236
|
+
phase.value = "select";
|
|
237
|
+
error.value = null;
|
|
238
|
+
totpCode.value = "";
|
|
239
|
+
recoveryCode.value = "";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
defineExpose({ setLoading, setError, setSuccess, reset, phase });
|
|
243
|
+
</script>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { mount } from "@vue/test-utils";
|
|
3
|
+
import SidRegistrationForm from "./SidRegistrationForm.vue";
|
|
4
|
+
|
|
5
|
+
const quasarStubs = {
|
|
6
|
+
"q-card": { template: "<div><slot /></div>" },
|
|
7
|
+
"q-card-section": { template: "<div><slot /></div>" },
|
|
8
|
+
"q-separator": { template: "<hr />" },
|
|
9
|
+
"q-banner": { template: "<div><slot /></div>" },
|
|
10
|
+
"q-icon": { template: "<span />" },
|
|
11
|
+
"q-chip": { template: "<span />" },
|
|
12
|
+
"q-form": {
|
|
13
|
+
template: "<form @submit.prevent=\"$emit('submit')\"><slot /></form>",
|
|
14
|
+
emits: ["submit"],
|
|
15
|
+
},
|
|
16
|
+
"q-input": {
|
|
17
|
+
props: ["modelValue"],
|
|
18
|
+
emits: ["update:modelValue"],
|
|
19
|
+
template:
|
|
20
|
+
'<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
|
|
21
|
+
},
|
|
22
|
+
"q-btn": { template: '<button type="submit"><slot /></button>' },
|
|
23
|
+
"sid-principal-input": {
|
|
24
|
+
props: ["modelValue", "disable", "label"],
|
|
25
|
+
emits: ["update:modelValue", "update:principal-type"],
|
|
26
|
+
template:
|
|
27
|
+
'<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const registerFn = vi.fn().mockResolvedValue(undefined);
|
|
32
|
+
|
|
33
|
+
function mountForm(props = {}) {
|
|
34
|
+
return mount(SidRegistrationForm, {
|
|
35
|
+
props: { registerFn, ...props },
|
|
36
|
+
global: { stubs: quasarStubs },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("SidRegistrationForm", () => {
|
|
41
|
+
it("renders with default title", () => {
|
|
42
|
+
const w = mountForm();
|
|
43
|
+
expect(w.text()).toContain("Create account");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders custom title", () => {
|
|
47
|
+
const w = mountForm({ title: "Join us" });
|
|
48
|
+
expect(w.text()).toContain("Join us");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("shows invite warning when required and no code", () => {
|
|
52
|
+
const w = mountForm({ inviteRequired: true });
|
|
53
|
+
expect(w.text()).toContain("Registration requires an invite code");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("hides invite field when not required and no code", () => {
|
|
57
|
+
const w = mountForm({ inviteRequired: false });
|
|
58
|
+
expect(w.text()).not.toContain("Invite code");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows invite field when code provided", () => {
|
|
62
|
+
const w = mountForm({ initialInviteCode: "ABCD1234" });
|
|
63
|
+
// showInviteField computed = true when code exists → invite input rendered
|
|
64
|
+
// q-input stub renders as <input>, look for it in the rendered HTML
|
|
65
|
+
const inputs = w.findAll("input");
|
|
66
|
+
// At minimum: invite, principal, password, confirm = 4 inputs.
|
|
67
|
+
// (given_name dropped — handled via OrgClaimPolicy after first login.)
|
|
68
|
+
expect(inputs.length).toBeGreaterThanOrEqual(4);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("renders footer slot", () => {
|
|
72
|
+
const w = mount(SidRegistrationForm, {
|
|
73
|
+
props: { registerFn },
|
|
74
|
+
global: { stubs: quasarStubs },
|
|
75
|
+
slots: { footer: "<div>Sign in instead</div>" },
|
|
76
|
+
});
|
|
77
|
+
expect(w.text()).toContain("Sign in instead");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-card flat bordered>
|
|
3
|
+
<q-card-section>
|
|
4
|
+
<slot name="header">
|
|
5
|
+
<div class="text-h6">{{ title }}</div>
|
|
6
|
+
<div class="text-caption text-grey">{{ subtitle }}</div>
|
|
7
|
+
</slot>
|
|
8
|
+
</q-card-section>
|
|
9
|
+
|
|
10
|
+
<q-separator />
|
|
11
|
+
|
|
12
|
+
<q-card-section>
|
|
13
|
+
<q-banner
|
|
14
|
+
v-if="inviteRequired && !inviteCode"
|
|
15
|
+
class="bg-warning text-dark q-mb-md"
|
|
16
|
+
rounded
|
|
17
|
+
dense
|
|
18
|
+
icon="sym_o_lock"
|
|
19
|
+
>
|
|
20
|
+
{{ inviteRequiredText }}
|
|
21
|
+
</q-banner>
|
|
22
|
+
|
|
23
|
+
<q-banner
|
|
24
|
+
v-if="error"
|
|
25
|
+
class="bg-negative text-white q-mb-md"
|
|
26
|
+
rounded
|
|
27
|
+
dense
|
|
28
|
+
>
|
|
29
|
+
{{ error }}
|
|
30
|
+
</q-banner>
|
|
31
|
+
|
|
32
|
+
<q-banner
|
|
33
|
+
v-if="success"
|
|
34
|
+
class="bg-positive text-white q-mb-md"
|
|
35
|
+
rounded
|
|
36
|
+
dense
|
|
37
|
+
>
|
|
38
|
+
{{ successText }}
|
|
39
|
+
</q-banner>
|
|
40
|
+
|
|
41
|
+
<q-form @submit.prevent="onSubmit" class="q-gutter-y-md">
|
|
42
|
+
<q-input
|
|
43
|
+
v-if="showInviteField"
|
|
44
|
+
v-model="inviteCode"
|
|
45
|
+
:label="inviteCodeLabel"
|
|
46
|
+
outlined
|
|
47
|
+
:disable="loading || inviteFromUrl"
|
|
48
|
+
maxlength="8"
|
|
49
|
+
mask="XXXXXXXX"
|
|
50
|
+
:rules="
|
|
51
|
+
inviteRequired ? [(v: string) => !!v || inviteCodeRequiredText] : []
|
|
52
|
+
"
|
|
53
|
+
:hint="inviteCodeHint"
|
|
54
|
+
>
|
|
55
|
+
<template v-slot:prepend>
|
|
56
|
+
<q-icon name="sym_o_vpn_key" />
|
|
57
|
+
</template>
|
|
58
|
+
<template v-slot:append>
|
|
59
|
+
<q-chip
|
|
60
|
+
v-if="inviteFromUrl"
|
|
61
|
+
color="positive"
|
|
62
|
+
text-color="white"
|
|
63
|
+
dense
|
|
64
|
+
size="sm"
|
|
65
|
+
icon="sym_o_link"
|
|
66
|
+
:label="inviteFromUrlLabel"
|
|
67
|
+
/>
|
|
68
|
+
</template>
|
|
69
|
+
</q-input>
|
|
70
|
+
|
|
71
|
+
<sid-principal-input
|
|
72
|
+
v-model="identifier"
|
|
73
|
+
:disable="loading"
|
|
74
|
+
:label="identifierLabel"
|
|
75
|
+
:lazy-rules="false"
|
|
76
|
+
@update:principal-type="principalType = $event"
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<slot name="extra-fields" />
|
|
80
|
+
|
|
81
|
+
<q-input
|
|
82
|
+
ref="passwordRef"
|
|
83
|
+
v-model="password"
|
|
84
|
+
:type="showPassword ? 'text' : 'password'"
|
|
85
|
+
:label="passwordLabel"
|
|
86
|
+
outlined
|
|
87
|
+
:disable="loading"
|
|
88
|
+
:lazy-rules="false"
|
|
89
|
+
:rules="passwordRules"
|
|
90
|
+
@update:model-value="onPasswordInput"
|
|
91
|
+
>
|
|
92
|
+
<template v-slot:prepend>
|
|
93
|
+
<q-icon name="sym_o_lock" />
|
|
94
|
+
</template>
|
|
95
|
+
<template v-slot:append>
|
|
96
|
+
<q-icon
|
|
97
|
+
:name="showPassword ? 'sym_o_visibility_off' : 'sym_o_visibility'"
|
|
98
|
+
class="cursor-pointer"
|
|
99
|
+
@click="showPassword = !showPassword"
|
|
100
|
+
/>
|
|
101
|
+
</template>
|
|
102
|
+
</q-input>
|
|
103
|
+
|
|
104
|
+
<q-input
|
|
105
|
+
ref="confirmRef"
|
|
106
|
+
v-model="confirmPassword"
|
|
107
|
+
:type="showPassword ? 'text' : 'password'"
|
|
108
|
+
:label="confirmPasswordLabel"
|
|
109
|
+
outlined
|
|
110
|
+
:disable="loading"
|
|
111
|
+
:lazy-rules="false"
|
|
112
|
+
:rules="[
|
|
113
|
+
(v: string) => !!v || passwordRequiredText,
|
|
114
|
+
(v: string) => v === password || passwordsMismatchText,
|
|
115
|
+
]"
|
|
116
|
+
>
|
|
117
|
+
<template v-slot:prepend>
|
|
118
|
+
<q-icon name="sym_o_lock" />
|
|
119
|
+
</template>
|
|
120
|
+
</q-input>
|
|
121
|
+
|
|
122
|
+
<q-btn
|
|
123
|
+
type="submit"
|
|
124
|
+
:label="submitLabel"
|
|
125
|
+
color="primary"
|
|
126
|
+
class="full-width"
|
|
127
|
+
:loading="loading"
|
|
128
|
+
:disable="
|
|
129
|
+
!identifier ||
|
|
130
|
+
!password ||
|
|
131
|
+
!confirmPassword ||
|
|
132
|
+
(inviteRequired && !inviteCode)
|
|
133
|
+
"
|
|
134
|
+
/>
|
|
135
|
+
</q-form>
|
|
136
|
+
|
|
137
|
+
<slot name="links">
|
|
138
|
+
<div v-if="loginUrl" class="text-center q-mt-md text-body2">
|
|
139
|
+
{{ haveAccountText }}
|
|
140
|
+
<router-link :to="loginUrl" class="text-primary">
|
|
141
|
+
{{ loginLinkText }}
|
|
142
|
+
</router-link>
|
|
143
|
+
</div>
|
|
144
|
+
</slot>
|
|
145
|
+
</q-card-section>
|
|
146
|
+
|
|
147
|
+
<slot name="footer" />
|
|
148
|
+
</q-card>
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<script setup lang="ts">
|
|
152
|
+
import { ref, computed, watch } from "vue";
|
|
153
|
+
import { SidPrincipalInput } from "../../quasar";
|
|
154
|
+
|
|
155
|
+
// Minimal QInput shape — we only use validate(). Avoids importing 'quasar' at
|
|
156
|
+
// type level (it's a peer dep, not present during ui-core typecheck).
|
|
157
|
+
interface ValidatableInput {
|
|
158
|
+
validate: () => boolean | Promise<boolean>;
|
|
159
|
+
}
|
|
160
|
+
import type { PrincipalType } from "../../quasar";
|
|
161
|
+
import { normalizePrincipal } from "../../index";
|
|
162
|
+
|
|
163
|
+
void SidPrincipalInput;
|
|
164
|
+
|
|
165
|
+
const props = withDefaults(
|
|
166
|
+
defineProps<{
|
|
167
|
+
/** Card title */
|
|
168
|
+
title?: string;
|
|
169
|
+
/** Card subtitle */
|
|
170
|
+
subtitle?: string;
|
|
171
|
+
/** Submit button label */
|
|
172
|
+
submitLabel?: string;
|
|
173
|
+
/** Identifier input label */
|
|
174
|
+
identifierLabel?: string;
|
|
175
|
+
/** Password input label */
|
|
176
|
+
passwordLabel?: string;
|
|
177
|
+
/** Confirm password input label */
|
|
178
|
+
confirmPasswordLabel?: string;
|
|
179
|
+
/** Password required validation text */
|
|
180
|
+
passwordRequiredText?: string;
|
|
181
|
+
/** Password min length validation text */
|
|
182
|
+
passwordMinLengthText?: string;
|
|
183
|
+
/** Passwords mismatch validation text */
|
|
184
|
+
passwordsMismatchText?: string;
|
|
185
|
+
/** Invite code input label */
|
|
186
|
+
inviteCodeLabel?: string;
|
|
187
|
+
/** Invite code required validation text */
|
|
188
|
+
inviteCodeRequiredText?: string;
|
|
189
|
+
/** Invite code hint text */
|
|
190
|
+
inviteCodeHint?: string;
|
|
191
|
+
/** Invite "from link" chip label */
|
|
192
|
+
inviteFromUrlLabel?: string;
|
|
193
|
+
/** Invite required banner text */
|
|
194
|
+
inviteRequiredText?: string;
|
|
195
|
+
/** Success banner text */
|
|
196
|
+
successText?: string;
|
|
197
|
+
/** "Already have an account?" text */
|
|
198
|
+
haveAccountText?: string;
|
|
199
|
+
/** "Sign in" link text */
|
|
200
|
+
loginLinkText?: string;
|
|
201
|
+
/** Route to login page (if set, shows login link) */
|
|
202
|
+
loginUrl?: string;
|
|
203
|
+
/** Whether invite code is required */
|
|
204
|
+
inviteRequired?: boolean;
|
|
205
|
+
/** Pre-filled invite code (e.g. from URL) */
|
|
206
|
+
initialInviteCode?: string;
|
|
207
|
+
/** External register function — receives (identifier, password, inviteCode?) */
|
|
208
|
+
registerFn: (
|
|
209
|
+
identifier: string,
|
|
210
|
+
password: string,
|
|
211
|
+
inviteCode?: string,
|
|
212
|
+
) => Promise<void>;
|
|
213
|
+
/** Min password length (default 12 — NIST SP 800-63B 4th draft baseline). */
|
|
214
|
+
passwordMinLength?: number;
|
|
215
|
+
}>(),
|
|
216
|
+
{
|
|
217
|
+
title: "Create account",
|
|
218
|
+
subtitle: "Set up your StructuredID account",
|
|
219
|
+
submitLabel: "Create account",
|
|
220
|
+
identifierLabel: "Email, phone, or username",
|
|
221
|
+
passwordLabel: "Password",
|
|
222
|
+
confirmPasswordLabel: "Confirm password",
|
|
223
|
+
passwordRequiredText: "Required",
|
|
224
|
+
passwordMinLengthText: "At least 12 characters with mixed case + digit",
|
|
225
|
+
passwordMinLength: 12,
|
|
226
|
+
passwordsMismatchText: "Passwords do not match",
|
|
227
|
+
inviteCodeLabel: "Invite code",
|
|
228
|
+
inviteCodeRequiredText: "Invite code required",
|
|
229
|
+
inviteCodeHint: "8-character code from your administrator",
|
|
230
|
+
inviteFromUrlLabel: "From link",
|
|
231
|
+
inviteRequiredText:
|
|
232
|
+
"Registration requires an invite code. Contact your administrator for access.",
|
|
233
|
+
successText: "Account created. Signing you in\u2026",
|
|
234
|
+
haveAccountText: "Already have an account?",
|
|
235
|
+
loginLinkText: "Sign in",
|
|
236
|
+
loginUrl: undefined,
|
|
237
|
+
inviteRequired: false,
|
|
238
|
+
initialInviteCode: "",
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const emit = defineEmits<{
|
|
243
|
+
success: [data: { identifier: string; principalType: string }];
|
|
244
|
+
error: [error: Error];
|
|
245
|
+
}>();
|
|
246
|
+
|
|
247
|
+
const identifier = ref("");
|
|
248
|
+
const principalType = ref<PrincipalType>("unknown");
|
|
249
|
+
const password = ref("");
|
|
250
|
+
const confirmPassword = ref("");
|
|
251
|
+
const showPassword = ref(false);
|
|
252
|
+
const loading = ref(false);
|
|
253
|
+
const error = ref<string | null>(null);
|
|
254
|
+
const success = ref(false);
|
|
255
|
+
|
|
256
|
+
const passwordRef = ref<ValidatableInput | null>(null);
|
|
257
|
+
const confirmRef = ref<ValidatableInput | null>(null);
|
|
258
|
+
|
|
259
|
+
// Password complexity (matches sid-pake-core ZKPP gadget A defaults).
|
|
260
|
+
// Server enforces via OPAQUE-ZKPP zero-knowledge proof; client also checks
|
|
261
|
+
// to give immediate UX feedback.
|
|
262
|
+
const passwordRules = computed(() => [
|
|
263
|
+
(v: string) => !!v || props.passwordRequiredText,
|
|
264
|
+
(v: string) =>
|
|
265
|
+
v.length >= props.passwordMinLength || props.passwordMinLengthText,
|
|
266
|
+
(v: string) => /[a-z]/.test(v) || props.passwordMinLengthText,
|
|
267
|
+
(v: string) => /[A-Z]/.test(v) || props.passwordMinLengthText,
|
|
268
|
+
(v: string) => /[0-9]/.test(v) || props.passwordMinLengthText,
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
// Re-validate confirm field whenever password changes (eager match feedback).
|
|
272
|
+
watch(password, () => {
|
|
273
|
+
if (confirmPassword.value) confirmRef.value?.validate();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
function onPasswordInput() {
|
|
277
|
+
// Trigger downstream confirm re-validation immediately (don't wait for blur).
|
|
278
|
+
if (confirmPassword.value) confirmRef.value?.validate();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const inviteCode = ref(props.initialInviteCode);
|
|
282
|
+
const inviteFromUrl = ref(!!props.initialInviteCode);
|
|
283
|
+
const showInviteField = computed(
|
|
284
|
+
() => props.inviteRequired || !!inviteCode.value,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
async function onSubmit() {
|
|
288
|
+
if (password.value !== confirmPassword.value) {
|
|
289
|
+
error.value = props.passwordsMismatchText;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
loading.value = true;
|
|
294
|
+
error.value = null;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const normalized = normalizePrincipal(identifier.value);
|
|
298
|
+
await props.registerFn(
|
|
299
|
+
normalized.value,
|
|
300
|
+
password.value,
|
|
301
|
+
inviteCode.value || undefined,
|
|
302
|
+
);
|
|
303
|
+
success.value = true;
|
|
304
|
+
emit("success", {
|
|
305
|
+
identifier: normalized.value,
|
|
306
|
+
principalType: normalized.type,
|
|
307
|
+
});
|
|
308
|
+
} catch (e) {
|
|
309
|
+
success.value = false;
|
|
310
|
+
const err = e instanceof Error ? e : new Error("Registration failed");
|
|
311
|
+
error.value = err.message;
|
|
312
|
+
emit("error", err);
|
|
313
|
+
} finally {
|
|
314
|
+
loading.value = false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Slot-based auth forms
|
|
2
|
+
export { default as SidLoginForm } from "./SidLoginForm.vue";
|
|
3
|
+
export { default as SidRegistrationForm } from "./SidRegistrationForm.vue";
|
|
4
|
+
export { default as SidMfaChallenge } from "./SidMfaChallenge.vue";
|
|
5
|
+
export type { MfaMethod } from "./SidMfaChallenge.vue";
|
|
6
|
+
|
|
7
|
+
// Shared page components
|
|
8
|
+
export { default as SessionList } from "./SessionList.vue";
|
|
9
|
+
export { default as ProfileEditor } from "./ProfileEditor.vue";
|
|
10
|
+
export { default as ConsentManager } from "./ConsentManager.vue";
|
|
11
|
+
export { default as SecuritySettings } from "./SecuritySettings.vue";
|