@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.
Files changed (156) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +47 -0
  3. package/dist/browser-vp/index.cjs +349 -0
  4. package/dist/browser-vp/index.d.cts +287 -0
  5. package/dist/browser-vp/index.d.ts +287 -0
  6. package/dist/browser-vp/index.js +349 -0
  7. package/dist/chunk-C3Q766CY.cjs +125 -0
  8. package/dist/chunk-J5YXRXB5.js +125 -0
  9. package/dist/index.cjs +19360 -0
  10. package/dist/index.d.cts +9536 -0
  11. package/dist/index.d.ts +9536 -0
  12. package/dist/index.js +19360 -0
  13. package/dist/storage-4CB2JTLW.cjs +22 -0
  14. package/dist/storage-THRWREZP.js +22 -0
  15. package/package.json +93 -0
  16. package/src/browser-vp/crypto.ts +254 -0
  17. package/src/browser-vp/index.test.ts +10 -0
  18. package/src/browser-vp/index.ts +118 -0
  19. package/src/browser-vp/prf.ts +181 -0
  20. package/src/browser-vp/storage.test.ts +9 -0
  21. package/src/browser-vp/storage.ts +165 -0
  22. package/src/browser-vp/types.ts +97 -0
  23. package/src/browser-vp/vp.test.ts +43 -0
  24. package/src/browser-vp/vp.ts +113 -0
  25. package/src/components/SidAvatar.vue +106 -0
  26. package/src/components/SidConfirmDialog.vue +125 -0
  27. package/src/components/SidStatusBadge.vue +71 -0
  28. package/src/components/index.ts +5 -0
  29. package/src/composables/index.ts +10 -0
  30. package/src/composables/useAuth.ts +40 -0
  31. package/src/composables/useBffAuth.ts +147 -0
  32. package/src/composables/useGrpc.test.ts +196 -0
  33. package/src/composables/useGrpc.ts +120 -0
  34. package/src/composables/useSession.ts +80 -0
  35. package/src/generated/google/api/http.ts +687 -0
  36. package/src/generated/google/protobuf/descriptor.ts +4865 -0
  37. package/src/generated/google/protobuf/duration.ts +230 -0
  38. package/src/generated/google/protobuf/empty.ts +94 -0
  39. package/src/generated/google/protobuf/struct.ts +484 -0
  40. package/src/generated/google/protobuf/timestamp.ts +287 -0
  41. package/src/generated/sid/v1/account/account.client.ts +527 -0
  42. package/src/generated/sid/v1/account/account.ts +3261 -0
  43. package/src/generated/sid/v1/admin/admin.client.ts +136 -0
  44. package/src/generated/sid/v1/admin/admin.ts +846 -0
  45. package/src/generated/sid/v1/admin/admin_roles.client.ts +181 -0
  46. package/src/generated/sid/v1/admin/admin_roles.ts +1606 -0
  47. package/src/generated/sid/v1/admin/analytics.client.ts +106 -0
  48. package/src/generated/sid/v1/admin/analytics.ts +1017 -0
  49. package/src/generated/sid/v1/admin/branding.client.ts +166 -0
  50. package/src/generated/sid/v1/admin/branding.ts +1906 -0
  51. package/src/generated/sid/v1/admin/enrollment.client.ts +165 -0
  52. package/src/generated/sid/v1/admin/enrollment.ts +1704 -0
  53. package/src/generated/sid/v1/admin/events.client.ts +195 -0
  54. package/src/generated/sid/v1/admin/events.ts +1900 -0
  55. package/src/generated/sid/v1/admin/flow.client.ts +183 -0
  56. package/src/generated/sid/v1/admin/flow.ts +1552 -0
  57. package/src/generated/sid/v1/admin/keys.client.ts +107 -0
  58. package/src/generated/sid/v1/admin/keys.ts +931 -0
  59. package/src/generated/sid/v1/admin/login_experience.client.ts +160 -0
  60. package/src/generated/sid/v1/admin/login_experience.ts +1061 -0
  61. package/src/generated/sid/v1/admin/migration.client.ts +127 -0
  62. package/src/generated/sid/v1/admin/migration.ts +1948 -0
  63. package/src/generated/sid/v1/admin/notifications.client.ts +128 -0
  64. package/src/generated/sid/v1/admin/notifications.ts +1332 -0
  65. package/src/generated/sid/v1/admin/operations.client.ts +241 -0
  66. package/src/generated/sid/v1/admin/operations.ts +2648 -0
  67. package/src/generated/sid/v1/admin/organizations.client.ts +57 -0
  68. package/src/generated/sid/v1/admin/organizations.ts +349 -0
  69. package/src/generated/sid/v1/admin/scopes.client.ts +171 -0
  70. package/src/generated/sid/v1/admin/scopes.ts +1324 -0
  71. package/src/generated/sid/v1/admin/security.client.ts +412 -0
  72. package/src/generated/sid/v1/admin/security.ts +4086 -0
  73. package/src/generated/sid/v1/admin/sessions.client.ts +114 -0
  74. package/src/generated/sid/v1/admin/sessions.ts +923 -0
  75. package/src/generated/sid/v1/admin/settings.client.ts +340 -0
  76. package/src/generated/sid/v1/admin/settings.ts +2395 -0
  77. package/src/generated/sid/v1/admin/user_profile.client.ts +57 -0
  78. package/src/generated/sid/v1/admin/user_profile.ts +730 -0
  79. package/src/generated/sid/v1/admin/webhooks.client.ts +269 -0
  80. package/src/generated/sid/v1/admin/webhooks.ts +2804 -0
  81. package/src/generated/sid/v1/attestation/attestation.client.ts +115 -0
  82. package/src/generated/sid/v1/attestation/attestation.ts +813 -0
  83. package/src/generated/sid/v1/authn/auth.client.ts +900 -0
  84. package/src/generated/sid/v1/authn/auth.ts +9287 -0
  85. package/src/generated/sid/v1/authn/flows.client.ts +413 -0
  86. package/src/generated/sid/v1/authn/flows.ts +3217 -0
  87. package/src/generated/sid/v1/authz/authz.client.ts +459 -0
  88. package/src/generated/sid/v1/authz/authz.ts +5343 -0
  89. package/src/generated/sid/v1/authz/governance.client.ts +102 -0
  90. package/src/generated/sid/v1/authz/governance.ts +818 -0
  91. package/src/generated/sid/v1/common/errors.ts +184 -0
  92. package/src/generated/sid/v1/common/pagination.ts +152 -0
  93. package/src/generated/sid/v1/common/transport.ts +227 -0
  94. package/src/generated/sid/v1/common/types.ts +151 -0
  95. package/src/generated/sid/v1/events/event_stream.client.ts +67 -0
  96. package/src/generated/sid/v1/events/event_stream.ts +374 -0
  97. package/src/generated/sid/v1/federation/upstream.client.ts +142 -0
  98. package/src/generated/sid/v1/federation/upstream.ts +1736 -0
  99. package/src/generated/sid/v1/identity/identity.client.ts +467 -0
  100. package/src/generated/sid/v1/identity/identity.ts +5613 -0
  101. package/src/generated/sid/v1/machine/machine_user.client.ts +219 -0
  102. package/src/generated/sid/v1/machine/machine_user.ts +2400 -0
  103. package/src/generated/sid/v1/machine/pat.client.ts +147 -0
  104. package/src/generated/sid/v1/machine/pat.ts +1268 -0
  105. package/src/generated/sid/v1/projects/projects.client.ts +286 -0
  106. package/src/generated/sid/v1/projects/projects.ts +2714 -0
  107. package/src/generated/sid/v1/scim/scim.client.ts +274 -0
  108. package/src/generated/sid/v1/scim/scim.ts +3043 -0
  109. package/src/generated/sid/v1/test/test.client.ts +232 -0
  110. package/src/generated/sid/v1/test/test.ts +1136 -0
  111. package/src/i18n/index.ts +40 -0
  112. package/src/index.ts +157 -0
  113. package/src/profile/auth.ts +15 -0
  114. package/src/profile/components/ConsentManager.vue +88 -0
  115. package/src/profile/components/ProfileEditor.vue +146 -0
  116. package/src/profile/components/SecuritySettings.vue +170 -0
  117. package/src/profile/components/SessionList.vue +100 -0
  118. package/src/profile/components/SidLoginForm.test.ts +80 -0
  119. package/src/profile/components/SidLoginForm.vue +230 -0
  120. package/src/profile/components/SidMfaChallenge.test.ts +127 -0
  121. package/src/profile/components/SidMfaChallenge.vue +243 -0
  122. package/src/profile/components/SidRegistrationForm.test.ts +79 -0
  123. package/src/profile/components/SidRegistrationForm.vue +317 -0
  124. package/src/profile/components/index.ts +11 -0
  125. package/src/profile/composables/index.ts +6 -0
  126. package/src/profile/composables/useAccountApi.ts +203 -0
  127. package/src/profile/composables/useAuthApi.opaque.test.ts +140 -0
  128. package/src/profile/composables/useAuthApi.ts +617 -0
  129. package/src/profile/composables/useIdentityApi.ts +123 -0
  130. package/src/profile/composables/useLoginSuccess.test.ts +112 -0
  131. package/src/profile/composables/useLoginSuccess.ts +65 -0
  132. package/src/profile/composables/useOpaqueAuth.test.ts +290 -0
  133. package/src/profile/composables/useOpaqueAuth.ts +191 -0
  134. package/src/profile/composables/useSecurityLevel.test.ts +198 -0
  135. package/src/profile/composables/useSecurityLevel.ts +324 -0
  136. package/src/profile/index.ts +18 -0
  137. package/src/quasar/SidPrincipalInput.vue +209 -0
  138. package/src/quasar/index.ts +8 -0
  139. package/src/stores/auth.test.ts +83 -0
  140. package/src/stores/auth.ts +92 -0
  141. package/src/stores/index.ts +1 -0
  142. package/src/transport/adaptive.test.ts +728 -0
  143. package/src/transport/adaptive.ts +308 -0
  144. package/src/transport/framing.test.ts +227 -0
  145. package/src/transport/framing.ts +98 -0
  146. package/src/transport/webtransport.test.ts +728 -0
  147. package/src/transport/webtransport.ts +323 -0
  148. package/src/types/auth.ts +58 -0
  149. package/src/types/index.ts +1 -0
  150. package/src/utils/formatters.test.ts +26 -0
  151. package/src/utils/formatters.ts +69 -0
  152. package/src/utils/index.ts +16 -0
  153. package/src/utils/normalizePrincipal.test.ts +188 -0
  154. package/src/utils/normalizePrincipal.ts +159 -0
  155. package/src/utils/validators.test.ts +75 -0
  156. package/src/utils/validators.ts +36 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * i18n key constants shared across all SID UI applications.
3
+ *
4
+ * These match the keys defined in sid-i18n Rust crate (locales/*.json).
5
+ * Apps import these constants instead of hardcoding strings,
6
+ * ensuring consistency between frontend and backend translations.
7
+ */
8
+
9
+ export const AUTH_KEYS = {
10
+ LOGIN_TITLE: "auth.login.title",
11
+ LOGIN_SUBMIT: "auth.login.submit",
12
+ REGISTER_TITLE: "auth.register.title",
13
+ REGISTER_SUBMIT: "auth.register.submit",
14
+ MFA_TITLE: "auth.mfa.title",
15
+ MAGIC_LINK_TITLE: "auth.magic_link.title",
16
+ DEVICE_AUTH_TITLE: "auth.device_auth.title",
17
+ PASSWORD_RESET_TITLE: "auth.password_reset.title",
18
+ LOGOUT_TITLE: "auth.logout.title",
19
+ LOGOUT_SUBMIT: "auth.logout.submit",
20
+ } as const;
21
+
22
+ export const ERROR_KEYS = {
23
+ INVALID_CREDENTIALS: "errors.auth.invalid_credentials",
24
+ ACCOUNT_LOCKED: "errors.auth.account_locked",
25
+ SESSION_EXPIRED: "errors.auth.session_expired",
26
+ MFA_REQUIRED: "errors.auth.mfa_required",
27
+ PERMISSION_DENIED: "errors.rbac.permission_denied",
28
+ NOT_FOUND: "errors.general.not_found",
29
+ INTERNAL_ERROR: "errors.general.internal_error",
30
+ RATE_LIMITED: "errors.general.rate_limited",
31
+ } as const;
32
+
33
+ export const COMMON_KEYS = {
34
+ SAVE: "common.buttons.save",
35
+ CANCEL: "common.buttons.cancel",
36
+ DELETE: "common.buttons.delete",
37
+ CONFIRM: "common.buttons.confirm",
38
+ LOADING: "common.labels.loading",
39
+ NO_RESULTS: "common.labels.no_results",
40
+ } as const;
package/src/index.ts ADDED
@@ -0,0 +1,157 @@
1
+ // Composables
2
+ export { useAuth } from "./composables/useAuth";
3
+ export {
4
+ useGrpc,
5
+ initGrpc,
6
+ getTransport,
7
+ closeTransport,
8
+ getTransportType,
9
+ type TransportType,
10
+ } from "./composables/useGrpc";
11
+ export { useSession, type SessionCallbacks } from "./composables/useSession";
12
+ export {
13
+ useBffAuth,
14
+ initBff,
15
+ getBffBaseUrl,
16
+ checkBffSession,
17
+ startBffLogin,
18
+ bffLogout,
19
+ type BffUserInfo,
20
+ } from "./composables/useBffAuth";
21
+
22
+ // Stores
23
+ export { useAuthStore } from "./stores/auth";
24
+
25
+ // Components — import from '@structured-id/ui-core/components' (raw .vue SFC)
26
+ // Consuming apps with their own Vue build pipeline import directly:
27
+ // import { SidStatusBadge } from '@structured-id/ui-core/components'
28
+
29
+ // Utils
30
+ export {
31
+ isValidEmail,
32
+ isValidPhone,
33
+ isValidUuid,
34
+ isValidUsername,
35
+ minLength,
36
+ maxLength,
37
+ isRequired,
38
+ formatDate,
39
+ formatRelativeTime,
40
+ truncate,
41
+ maskEmail,
42
+ } from "./utils";
43
+
44
+ export {
45
+ detectLoginInputType,
46
+ normalizePrincipal,
47
+ type LoginInputType,
48
+ type NormalizedPrincipal,
49
+ } from "./utils/normalizePrincipal";
50
+
51
+ // Types
52
+ export type { AuthState, SessionInfo, GrpcConfig } from "./types";
53
+
54
+ // i18n key constants
55
+ export { AUTH_KEYS, ERROR_KEYS, COMMON_KEYS } from "./i18n";
56
+
57
+ // Generated proto clients and types
58
+ export { AuthServiceClient } from "./generated/sid/v1/authn/auth.client";
59
+ export { IdentityServiceClient } from "./generated/sid/v1/identity/identity.client";
60
+ export { AccountServiceClient } from "./generated/sid/v1/account/account.client";
61
+ export { UpstreamServiceClient } from "./generated/sid/v1/federation/upstream.client";
62
+
63
+ // Re-export commonly used proto types
64
+ export type {
65
+ OpaqueLoginStartRequest,
66
+ OpaqueLoginStartResponse,
67
+ OpaqueLoginFinishRequest,
68
+ OpaqueLoginFinishResponse,
69
+ OpaqueRegistrationStartRequest,
70
+ OpaqueRegistrationStartResponse,
71
+ OpaqueRegistrationFinishRequest,
72
+ OpaqueRegistrationFinishResponse,
73
+ MfaVerifyResponse,
74
+ TotpEnrollmentChallenge,
75
+ FinishTotpEnrollmentResponse,
76
+ ResendSmsMfaResponse,
77
+ GenerateRecoveryCodesResponse,
78
+ VerifyOtpResponse,
79
+ ResendOtpResponse,
80
+ VerifyPhoneResponse,
81
+ ConfirmIdpLinkResponse,
82
+ UserOrganization,
83
+ ListUserOrganizationsResponse,
84
+ SelectOrganizationResponse,
85
+ GetTermsResponse,
86
+ AcceptTermsResponse,
87
+ CibaConsentResponse,
88
+ VerifyCaptchaResponse,
89
+ DynamicPrompt,
90
+ DynamicPromptField,
91
+ SubmitDynamicPromptResponse,
92
+ StepUpChallenge,
93
+ CompleteStepUpResponse,
94
+ DeleteMfaCredentialResponse,
95
+ ConfirmAccountDeletionResponse,
96
+ SubmitDeviceUserCodeResponse,
97
+ OAuth2AuthorizeResponse,
98
+ WebAuthnAuthenticationStartResponse,
99
+ WebAuthnAuthenticationFinishResponse,
100
+ WebAuthnRegistrationStartResponse,
101
+ WebAuthnRegistrationFinishResponse,
102
+ RequestMagicLinkResponse,
103
+ VerifyMagicLinkResponse,
104
+ RequestPasswordResetResponse,
105
+ VerifyPasswordResetResponse,
106
+ CompletePasswordResetResponse,
107
+ } from "./generated/sid/v1/authn/auth";
108
+
109
+ export type {
110
+ Profile,
111
+ Session,
112
+ Credential,
113
+ Principal,
114
+ } from "./generated/sid/v1/identity/identity";
115
+
116
+ export { StepUpMethod } from "./generated/sid/v1/authn/auth";
117
+
118
+ export {
119
+ ProfileStatus,
120
+ PrincipalType,
121
+ CredentialType,
122
+ AuthenticatorAttachment,
123
+ } from "./generated/sid/v1/identity/identity";
124
+
125
+ export type {
126
+ ConsentInfo,
127
+ ClaimConsent,
128
+ ActivityEvent,
129
+ ListConsentsResponse,
130
+ GetConsentDetailResponse,
131
+ ListActivityEventsResponse,
132
+ LinkedAccount,
133
+ ListLinkedAccountsResponse,
134
+ LinkAccountResponse,
135
+ ReauthenticateLinkResponse,
136
+ AvailableApp,
137
+ ListAvailableAppsResponse,
138
+ LaunchAppResponse,
139
+ PasswordlessStatus,
140
+ DisablePasswordlessResponse,
141
+ } from "./generated/sid/v1/account/account";
142
+
143
+ export {
144
+ ConsentStatus,
145
+ ClaimType,
146
+ ClaimConsentStatus,
147
+ ActivityEventStatus,
148
+ LinkedAccountTrustCategory,
149
+ LinkedAccountStatus,
150
+ } from "./generated/sid/v1/account/account";
151
+
152
+ export type { EnabledProviderInfo } from "./generated/sid/v1/federation/upstream";
153
+
154
+ export { Timestamp } from "./generated/google/protobuf/timestamp";
155
+
156
+ // Browser VP — import from '@structured-id/ui-core/browser-vp'
157
+ // Separate entry point: WebAuthn PRF, IndexedDB, VP creation (browser-only APIs)
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Auth metadata helper for profile-core composables.
3
+ *
4
+ * After BFF migration (#536i-3): returns empty metadata.
5
+ * The BFF proxy injects the Bearer token server-side from the httpOnly session cookie.
6
+ * Client JS never holds tokens — no authorization header sent from the browser.
7
+ */
8
+
9
+ /**
10
+ * Get gRPC metadata for authenticated requests.
11
+ * In BFF mode: returns empty object — BFF adds Authorization: Bearer automatically.
12
+ */
13
+ export function authMeta(): Record<string, string> {
14
+ return {};
15
+ }
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div>
3
+ <q-banner v-if="error" class="bg-negative text-white q-mb-md" rounded dense>
4
+ {{ error }}
5
+ </q-banner>
6
+
7
+ <div v-if="loading" class="text-center q-pa-lg">
8
+ <q-spinner-dots size="32px" color="primary" />
9
+ </div>
10
+
11
+ <div
12
+ v-else-if="consents.length === 0"
13
+ class="text-center text-grey q-pa-lg"
14
+ >
15
+ No connected applications
16
+ </div>
17
+
18
+ <div v-else class="row q-gutter-md">
19
+ <div
20
+ v-for="consent in consents"
21
+ :key="consent.siteId"
22
+ class="col-12 col-sm-6 col-md-4"
23
+ >
24
+ <q-card flat bordered>
25
+ <q-card-section class="row items-center q-gutter-sm">
26
+ <q-avatar size="32px" color="grey-3">
27
+ <img
28
+ v-if="consent.siteFavicon"
29
+ :src="consent.siteFavicon"
30
+ :alt="consent.siteName"
31
+ />
32
+ <q-icon v-else name="sym_o_language" color="grey" />
33
+ </q-avatar>
34
+ <div class="col">
35
+ <div class="text-subtitle2">{{ consent.siteName }}</div>
36
+ <div class="text-caption text-grey">
37
+ Connected {{ formatDate(consent.connectedAt) }}
38
+ </div>
39
+ </div>
40
+ <q-badge
41
+ :color="consent.status === activeStatus ? 'positive' : 'grey'"
42
+ :label="consent.status === activeStatus ? 'Active' : 'Inactive'"
43
+ />
44
+ </q-card-section>
45
+ <q-separator />
46
+ <q-card-actions align="right">
47
+ <q-btn
48
+ flat
49
+ dense
50
+ label="Details"
51
+ color="primary"
52
+ @click="$emit('view-detail', consent.siteId)"
53
+ />
54
+ <q-btn
55
+ flat
56
+ dense
57
+ label="Disconnect"
58
+ color="negative"
59
+ @click="$emit('disconnect', consent.siteId)"
60
+ />
61
+ </q-card-actions>
62
+ </q-card>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { type ConsentInfo, ConsentStatus, Timestamp } from "../../index";
70
+
71
+ defineProps<{
72
+ consents: ConsentInfo[];
73
+ loading: boolean;
74
+ error: string | null;
75
+ }>();
76
+
77
+ defineEmits<{
78
+ "view-detail": [siteId: string];
79
+ disconnect: [siteId: string];
80
+ }>();
81
+
82
+ const activeStatus = ConsentStatus.ACTIVE;
83
+
84
+ function formatDate(ts?: { seconds: bigint; nanos: number }): string {
85
+ if (!ts) return "";
86
+ return Timestamp.toDate(ts).toLocaleDateString();
87
+ }
88
+ </script>
@@ -0,0 +1,146 @@
1
+ <template>
2
+ <div>
3
+ <q-banner v-if="error" class="bg-negative text-white q-mb-md" rounded dense>
4
+ {{ error }}
5
+ </q-banner>
6
+
7
+ <div v-if="loading" class="text-center q-pa-lg">
8
+ <q-spinner-dots size="32px" color="primary" />
9
+ </div>
10
+
11
+ <q-form
12
+ v-else
13
+ @submit.prevent="$emit('save', formData)"
14
+ class="q-gutter-md"
15
+ >
16
+ <q-input
17
+ v-model="formData.givenName"
18
+ label="Given name"
19
+ outlined
20
+ :disable="saving"
21
+ >
22
+ <template v-slot:prepend>
23
+ <q-icon name="sym_o_badge" />
24
+ </template>
25
+ </q-input>
26
+
27
+ <q-input
28
+ v-model="formData.familyName"
29
+ label="Family name"
30
+ outlined
31
+ :disable="saving"
32
+ >
33
+ <template v-slot:prepend>
34
+ <q-icon name="sym_o_badge" />
35
+ </template>
36
+ </q-input>
37
+
38
+ <q-input
39
+ v-model="formData.middleName"
40
+ label="Middle name"
41
+ outlined
42
+ :disable="saving"
43
+ >
44
+ <template v-slot:prepend>
45
+ <q-icon name="sym_o_badge" />
46
+ </template>
47
+ </q-input>
48
+
49
+ <q-input
50
+ v-if="showAvatar"
51
+ v-model="formData.avatarUrl"
52
+ label="Avatar URL"
53
+ outlined
54
+ :disable="saving"
55
+ >
56
+ <template v-slot:prepend>
57
+ <q-icon name="sym_o_account_circle" />
58
+ </template>
59
+ </q-input>
60
+
61
+ <slot name="extra-fields" :form="formData" />
62
+
63
+ <q-input
64
+ v-if="username"
65
+ :model-value="username"
66
+ label="Username"
67
+ outlined
68
+ readonly
69
+ disable
70
+ >
71
+ <template v-slot:prepend>
72
+ <q-icon name="sym_o_alternate_email" />
73
+ </template>
74
+ </q-input>
75
+
76
+ <q-btn
77
+ type="submit"
78
+ label="Save changes"
79
+ color="primary"
80
+ :loading="saving"
81
+ :disable="!hasChanges"
82
+ />
83
+ </q-form>
84
+ </div>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ import { reactive, computed, watch } from "vue";
89
+ import type { Profile } from "../../index";
90
+
91
+ const props = withDefaults(
92
+ defineProps<{
93
+ profile: Profile | null;
94
+ loading: boolean;
95
+ saving: boolean;
96
+ error: string | null;
97
+ username?: string;
98
+ showAvatar?: boolean;
99
+ }>(),
100
+ {
101
+ username: "",
102
+ showAvatar: true,
103
+ },
104
+ );
105
+
106
+ defineEmits<{
107
+ save: [
108
+ data: {
109
+ givenName: string;
110
+ familyName: string;
111
+ middleName: string;
112
+ avatarUrl: string;
113
+ },
114
+ ];
115
+ }>();
116
+
117
+ const formData = reactive({
118
+ givenName: "",
119
+ familyName: "",
120
+ middleName: "",
121
+ avatarUrl: "",
122
+ });
123
+
124
+ const hasChanges = computed(() => {
125
+ if (!props.profile) return false;
126
+ return (
127
+ formData.givenName !== (props.profile.givenName || "") ||
128
+ formData.familyName !== (props.profile.familyName || "") ||
129
+ formData.middleName !== (props.profile.middleName || "") ||
130
+ formData.avatarUrl !== (props.profile.avatarUrl || "")
131
+ );
132
+ });
133
+
134
+ watch(
135
+ () => props.profile,
136
+ (p) => {
137
+ if (p) {
138
+ formData.givenName = p.givenName || "";
139
+ formData.familyName = p.familyName || "";
140
+ formData.middleName = p.middleName || "";
141
+ formData.avatarUrl = p.avatarUrl || "";
142
+ }
143
+ },
144
+ { immediate: true },
145
+ );
146
+ </script>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <div>
3
+ <q-banner v-if="error" class="bg-negative text-white q-mb-md" rounded dense>
4
+ {{ error }}
5
+ </q-banner>
6
+
7
+ <!-- Security & Recoverability overview -->
8
+ <div class="row q-gutter-md q-mb-md">
9
+ <q-card flat bordered class="col">
10
+ <q-card-section>
11
+ <div class="text-subtitle2">Security level</div>
12
+ <div :class="`text-${securityColor}`" class="text-h5">
13
+ {{ securityLabel }}
14
+ </div>
15
+ <q-linear-progress
16
+ :value="securityProgress"
17
+ :color="securityColor"
18
+ class="q-mt-sm"
19
+ />
20
+ <div class="text-caption text-grey q-mt-xs">
21
+ {{ factorCount }} factor{{ factorCount !== 1 ? "s" : "" }},
22
+ {{ categories.size }} categor{{
23
+ categories.size !== 1 ? "ies" : "y"
24
+ }}
25
+ </div>
26
+ </q-card-section>
27
+ </q-card>
28
+
29
+ <q-card flat bordered class="col">
30
+ <q-card-section>
31
+ <div class="text-subtitle2">Recoverability</div>
32
+ <div :class="`text-${recoverabilityColor}`" class="text-h5">
33
+ {{ recoverabilityLabel }}
34
+ </div>
35
+ <q-linear-progress
36
+ :value="recoverabilityProgress"
37
+ :color="recoverabilityColor"
38
+ class="q-mt-sm"
39
+ />
40
+ <div class="text-caption text-grey q-mt-xs">
41
+ {{ recoveryMethods.length }} recovery method{{
42
+ recoveryMethods.length !== 1 ? "s" : ""
43
+ }}
44
+ </div>
45
+ </q-card-section>
46
+ </q-card>
47
+ </div>
48
+
49
+ <!-- Credentials list -->
50
+ <div v-if="loading" class="text-center q-pa-lg">
51
+ <q-spinner-dots size="32px" color="primary" />
52
+ </div>
53
+
54
+ <q-list v-else-if="credentials.length > 0" separator>
55
+ <q-item v-for="cred in credentials" :key="cred.id">
56
+ <q-item-section avatar>
57
+ <q-icon :name="credIcon(cred.type)" :color="credColor(cred.type)" />
58
+ </q-item-section>
59
+ <q-item-section>
60
+ <q-item-label>
61
+ {{ cred.label || credTypeName(cred.type) }}
62
+ </q-item-label>
63
+ <q-item-label caption>
64
+ {{ credTypeName(cred.type) }}
65
+ <span v-if="cred.lastUsedAt">
66
+ &middot; Last used {{ formatRelative(cred.lastUsedAt) }}
67
+ </span>
68
+ </q-item-label>
69
+ </q-item-section>
70
+ <q-item-section side>
71
+ <div class="row q-gutter-xs">
72
+ <q-btn
73
+ flat
74
+ dense
75
+ icon="sym_o_edit"
76
+ @click="$emit('edit-label', cred)"
77
+ />
78
+ <q-btn
79
+ flat
80
+ dense
81
+ icon="sym_o_delete"
82
+ color="negative"
83
+ @click="$emit('revoke', cred)"
84
+ />
85
+ </div>
86
+ </q-item-section>
87
+ </q-item>
88
+ </q-list>
89
+
90
+ <div v-else class="text-center text-grey q-pa-lg">
91
+ No credentials configured
92
+ </div>
93
+
94
+ <slot name="actions" />
95
+ </div>
96
+ </template>
97
+
98
+ <script setup lang="ts">
99
+ import { type Credential, CredentialType, Timestamp } from "../../index";
100
+ import type { FactorCategory } from "../composables/useSecurityLevel";
101
+
102
+ defineProps<{
103
+ credentials: Credential[];
104
+ loading: boolean;
105
+ error: string | null;
106
+ securityLabel: string;
107
+ securityColor: string;
108
+ securityProgress: number;
109
+ factorCount: number;
110
+ categories: Set<FactorCategory>;
111
+ recoverabilityLabel: string;
112
+ recoverabilityColor: string;
113
+ recoverabilityProgress: number;
114
+ recoveryMethods: string[];
115
+ }>();
116
+
117
+ defineEmits<{
118
+ "edit-label": [cred: Credential];
119
+ revoke: [cred: Credential];
120
+ }>();
121
+
122
+ const CRED_ICONS: Partial<Record<CredentialType, string>> = {
123
+ [CredentialType.OPAQUE]: "sym_o_key",
124
+ [CredentialType.LEGACY_HASH]: "sym_o_key",
125
+ [CredentialType.TOTP]: "sym_o_timer",
126
+ [CredentialType.WEBAUTHN]: "sym_o_passkey",
127
+ [CredentialType.RECOVERY]: "sym_o_vpn_key",
128
+ };
129
+
130
+ function credIcon(type: CredentialType): string {
131
+ return CRED_ICONS[type] || "sym_o_security";
132
+ }
133
+
134
+ const CRED_COLORS: Partial<Record<CredentialType, string>> = {
135
+ [CredentialType.OPAQUE]: "blue",
136
+ [CredentialType.LEGACY_HASH]: "orange",
137
+ [CredentialType.TOTP]: "teal",
138
+ [CredentialType.WEBAUTHN]: "purple",
139
+ [CredentialType.RECOVERY]: "amber",
140
+ };
141
+
142
+ function credColor(type: CredentialType): string {
143
+ return CRED_COLORS[type] || "grey";
144
+ }
145
+
146
+ const CRED_TYPE_NAMES: Partial<Record<CredentialType, string>> = {
147
+ [CredentialType.OPAQUE]: "Password",
148
+ [CredentialType.LEGACY_HASH]: "Legacy password",
149
+ [CredentialType.TOTP]: "Authenticator app",
150
+ [CredentialType.WEBAUTHN]: "Passkey",
151
+ [CredentialType.RECOVERY]: "Recovery codes",
152
+ };
153
+
154
+ function credTypeName(type: CredentialType): string {
155
+ return CRED_TYPE_NAMES[type] || String(type);
156
+ }
157
+
158
+ function formatRelative(ts?: { seconds: bigint; nanos: number }): string {
159
+ if (!ts) return "";
160
+ const date = Timestamp.toDate(ts);
161
+ const diff = Date.now() - date.getTime();
162
+ const mins = Math.floor(diff / 60_000);
163
+ if (mins < 1) return "just now";
164
+ if (mins < 60) return `${mins}m ago`;
165
+ const hours = Math.floor(mins / 60);
166
+ if (hours < 24) return `${hours}h ago`;
167
+ const days = Math.floor(hours / 24);
168
+ return `${days}d ago`;
169
+ }
170
+ </script>