@abraca/nuxt 2.0.1 → 2.0.4
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/dist/module.d.mts +18 -7
- package/dist/module.json +1 -1
- package/dist/module.mjs +21 -5
- package/dist/runtime/assets/aware-tokens.css +1 -0
- package/dist/runtime/components/AAccountSwitcherModal.d.vue.ts +16 -1
- package/dist/runtime/components/AAccountSwitcherModal.vue +33 -4
- package/dist/runtime/components/AAccountSwitcherModal.vue.d.ts +16 -1
- package/dist/runtime/components/AAuthLinkLanding.d.vue.ts +3 -0
- package/dist/runtime/components/AAuthLinkLanding.vue +85 -0
- package/dist/runtime/components/AAuthLinkLanding.vue.d.ts +3 -0
- package/dist/runtime/components/AClaimAccountModal.d.vue.ts +7 -1
- package/dist/runtime/components/AClaimAccountModal.vue +28 -13
- package/dist/runtime/components/AClaimAccountModal.vue.d.ts +7 -1
- package/dist/runtime/components/AEmailVerifyConfirmModal.d.vue.ts +30 -0
- package/dist/runtime/components/AEmailVerifyConfirmModal.vue +100 -0
- package/dist/runtime/components/AEmailVerifyConfirmModal.vue.d.ts +30 -0
- package/dist/runtime/components/AEmailVerifyRequestCard.d.vue.ts +22 -0
- package/dist/runtime/components/AEmailVerifyRequestCard.vue +65 -0
- package/dist/runtime/components/AEmailVerifyRequestCard.vue.d.ts +22 -0
- package/dist/runtime/components/AMnemonicLoginModal.d.vue.ts +1 -1
- package/dist/runtime/components/AMnemonicLoginModal.vue.d.ts +1 -1
- package/dist/runtime/components/ANodePanel.vue +2 -0
- package/dist/runtime/components/ANotificationBell.d.vue.ts +2 -2
- package/dist/runtime/components/ANotificationBell.vue.d.ts +2 -2
- package/dist/runtime/components/APasswordChangeModal.d.vue.ts +28 -0
- package/dist/runtime/components/APasswordChangeModal.vue +178 -0
- package/dist/runtime/components/APasswordChangeModal.vue.d.ts +28 -0
- package/dist/runtime/components/APasswordLoginModal.d.vue.ts +42 -0
- package/dist/runtime/components/APasswordLoginModal.vue +177 -0
- package/dist/runtime/components/APasswordLoginModal.vue.d.ts +42 -0
- package/dist/runtime/components/APasswordRegisterModal.d.vue.ts +49 -0
- package/dist/runtime/components/APasswordRegisterModal.vue +262 -0
- package/dist/runtime/components/APasswordRegisterModal.vue.d.ts +49 -0
- package/dist/runtime/components/APasswordResetConfirmModal.d.vue.ts +31 -0
- package/dist/runtime/components/APasswordResetConfirmModal.vue +154 -0
- package/dist/runtime/components/APasswordResetConfirmModal.vue.d.ts +31 -0
- package/dist/runtime/components/APasswordResetRequestModal.d.vue.ts +35 -0
- package/dist/runtime/components/APasswordResetRequestModal.vue +113 -0
- package/dist/runtime/components/APasswordResetRequestModal.vue.d.ts +35 -0
- package/dist/runtime/components/ASetPasswordCard.d.vue.ts +26 -0
- package/dist/runtime/components/ASetPasswordCard.vue +139 -0
- package/dist/runtime/components/ASetPasswordCard.vue.d.ts +26 -0
- package/dist/runtime/components/aware/AAccordion.d.vue.ts +2 -0
- package/dist/runtime/components/aware/AAccordion.vue +11 -1
- package/dist/runtime/components/aware/AAccordion.vue.d.ts +2 -0
- package/dist/runtime/components/aware/AButton.vue +3 -3
- package/dist/runtime/components/aware/ACollapsible.d.vue.ts +2 -0
- package/dist/runtime/components/aware/ACollapsible.vue +9 -1
- package/dist/runtime/components/aware/ACollapsible.vue.d.ts +2 -0
- package/dist/runtime/components/aware/AGlobalFocusLayer.vue +1 -1
- package/dist/runtime/components/aware/AHoverItem.vue +28 -3
- package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
- package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
- package/dist/runtime/components/aware/AModal.d.vue.ts +2 -0
- package/dist/runtime/components/aware/AModal.vue +9 -1
- package/dist/runtime/components/aware/AModal.vue.d.ts +2 -0
- package/dist/runtime/components/aware/APresenceBlobs.vue +1 -1
- package/dist/runtime/components/aware/APresenceCursors.vue +1 -1
- package/dist/runtime/components/aware/AScroll.d.vue.ts +2 -0
- package/dist/runtime/components/aware/AScroll.vue +13 -3
- package/dist/runtime/components/aware/AScroll.vue.d.ts +2 -0
- package/dist/runtime/components/aware/ASlideover.d.vue.ts +2 -0
- package/dist/runtime/components/aware/ASlideover.vue +9 -1
- package/dist/runtime/components/aware/ASlideover.vue.d.ts +2 -0
- package/dist/runtime/components/aware/ASlider.vue +1 -0
- package/dist/runtime/components/aware/ATabs.d.vue.ts +2 -0
- package/dist/runtime/components/aware/ATabs.vue +9 -1
- package/dist/runtime/components/aware/ATabs.vue.d.ts +2 -0
- package/dist/runtime/components/chat/ANodeChatPanel.vue +1 -0
- package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +2 -2
- package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +2 -2
- package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +2 -2
- package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +2 -2
- package/dist/runtime/components/shell/AUserMenu.d.vue.ts +2 -2
- package/dist/runtime/components/shell/AUserMenu.vue.d.ts +2 -2
- package/dist/runtime/components/shell/AUserProfilePopover.d.vue.ts +1 -1
- package/dist/runtime/components/shell/AUserProfilePopover.vue.d.ts +1 -1
- package/dist/runtime/composables/useAAField.js +7 -4
- package/dist/runtime/composables/useAAFocus.js +10 -5
- package/dist/runtime/composables/useAAFollowAnchor.js +68 -34
- package/dist/runtime/composables/useAAFollowPeer.d.ts +7 -4
- package/dist/runtime/composables/useAAFollowPeer.js +60 -11
- package/dist/runtime/composables/useAAViewport.d.ts +1 -1
- package/dist/runtime/composables/useAbracadabraAuth.d.ts +2 -0
- package/dist/runtime/composables/useAbracadabraAuth.js +2 -0
- package/dist/runtime/composables/useEmailVerification.d.ts +40 -26
- package/dist/runtime/composables/useEmailVerification.js +95 -43
- package/dist/runtime/composables/usePasswordAuth.d.ts +64 -0
- package/dist/runtime/composables/usePasswordAuth.js +126 -0
- package/dist/runtime/composables/useTiptapHistory.d.ts +2 -2
- package/dist/runtime/composables/useTiptapHistory.js +5 -5
- package/dist/runtime/extensions/views/MetaFieldView.vue +23 -6
- package/dist/runtime/plugin-abracadabra.client.js +57 -8
- package/dist/runtime/plugin-abracadabra.server.js +2 -0
- package/dist/runtime/server/plugins/abracadabra-service.js +20 -9
- package/dist/runtime/types.d.ts +11 -0
- package/dist/runtime/utils/awareRingStyle.js +1 -1
- package/package.json +7 -7
- package/dist/runtime/components/renderers/ASpatialRenderer.d.vue.ts +0 -19
- package/dist/runtime/components/renderers/ASpatialRenderer.vue +0 -459
- package/dist/runtime/components/renderers/ASpatialRenderer.vue.d.ts +0 -19
- package/dist/runtime/components/renderers/spatial/SpatialGround.d.vue.ts +0 -20
- package/dist/runtime/components/renderers/spatial/SpatialGround.vue +0 -26
- package/dist/runtime/components/renderers/spatial/SpatialGround.vue.d.ts +0 -20
- package/dist/runtime/components/renderers/spatial/SpatialObject.d.vue.ts +0 -17
- package/dist/runtime/components/renderers/spatial/SpatialObject.vue +0 -257
- package/dist/runtime/components/renderers/spatial/SpatialObject.vue.d.ts +0 -17
- package/dist/runtime/components/renderers/spatial/SpatialSceneBridge.d.vue.ts +0 -15
- package/dist/runtime/components/renderers/spatial/SpatialSceneBridge.vue +0 -18
- package/dist/runtime/components/renderers/spatial/SpatialSceneBridge.vue.d.ts +0 -15
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.d.vue.ts +0 -16
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue +0 -66
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue.d.ts +0 -16
- package/dist/runtime/components/renderers/spatial/SpatialUserAvatar.d.vue.ts +0 -8
- package/dist/runtime/components/renderers/spatial/SpatialUserAvatar.vue +0 -53
- package/dist/runtime/components/renderers/spatial/SpatialUserAvatar.vue.d.ts +0 -8
- package/dist/runtime/composables/useSpatialCamera.d.ts +0 -16
- package/dist/runtime/composables/useSpatialCamera.js +0 -175
- package/dist/runtime/composables/useSpatialDrag.d.ts +0 -14
- package/dist/runtime/composables/useSpatialDrag.js +0 -137
|
@@ -2,17 +2,42 @@ import { computed, onScopeDispose, ref, watch } from "vue";
|
|
|
2
2
|
import { useAbracadabra } from "./useAbracadabra.js";
|
|
3
3
|
import { useSyncedMap } from "./useYDoc.js";
|
|
4
4
|
import { useAwareness } from "./useAwareness.js";
|
|
5
|
-
const
|
|
5
|
+
const _followingPublicKey = ref(null);
|
|
6
|
+
const _followingClientIdFallback = ref(null);
|
|
6
7
|
export function useAAFollowPeer(options = {}) {
|
|
7
8
|
const { doc, provider } = useAbracadabra();
|
|
8
9
|
const { data, set } = useSyncedMap(doc, "aware-ui");
|
|
9
10
|
const { states } = useAwareness();
|
|
10
|
-
const followingClientId = _followingClientId;
|
|
11
11
|
let unsubInteraction = null;
|
|
12
|
+
const followingClientId = computed(() => {
|
|
13
|
+
const pk = _followingPublicKey.value;
|
|
14
|
+
if (pk) {
|
|
15
|
+
for (const [cid, st] of states.value.entries()) {
|
|
16
|
+
const peerPk = st?.user?.publicKey;
|
|
17
|
+
if (peerPk === pk) return cid;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return _followingClientIdFallback.value;
|
|
22
|
+
});
|
|
23
|
+
const isFollowing = computed(
|
|
24
|
+
() => _followingPublicKey.value != null || _followingClientIdFallback.value != null
|
|
25
|
+
);
|
|
26
|
+
const followingPublicKey = computed(() => _followingPublicKey.value);
|
|
27
|
+
const followingUserCache = ref(null);
|
|
12
28
|
const followingUser = computed(() => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
const cid = followingClientId.value;
|
|
30
|
+
if (cid != null) {
|
|
31
|
+
const state = states.value.get(cid);
|
|
32
|
+
if (state?.user) return state.user;
|
|
33
|
+
}
|
|
34
|
+
return followingUserCache.value;
|
|
35
|
+
});
|
|
36
|
+
watch(followingUser, (user) => {
|
|
37
|
+
if (user) followingUserCache.value = { ...user };
|
|
38
|
+
}, { flush: "post" });
|
|
39
|
+
watch(isFollowing, (active) => {
|
|
40
|
+
if (!active) followingUserCache.value = null;
|
|
16
41
|
});
|
|
17
42
|
function detachInteraction() {
|
|
18
43
|
unsubInteraction?.();
|
|
@@ -31,14 +56,31 @@ export function useAAFollowPeer(options = {}) {
|
|
|
31
56
|
window.removeEventListener("wheel", onBreak);
|
|
32
57
|
};
|
|
33
58
|
}
|
|
34
|
-
function follow(
|
|
35
|
-
if (
|
|
36
|
-
|
|
59
|
+
function follow(clientIdOrPublicKey) {
|
|
60
|
+
if (typeof clientIdOrPublicKey === "string") {
|
|
61
|
+
if (_followingPublicKey.value === clientIdOrPublicKey) return;
|
|
62
|
+
_followingPublicKey.value = clientIdOrPublicKey;
|
|
63
|
+
_followingClientIdFallback.value = null;
|
|
64
|
+
} else {
|
|
65
|
+
const cid = clientIdOrPublicKey;
|
|
66
|
+
const state = states.value.get(cid);
|
|
67
|
+
const pk = state?.user?.publicKey;
|
|
68
|
+
if (pk) {
|
|
69
|
+
if (_followingPublicKey.value === pk) return;
|
|
70
|
+
_followingPublicKey.value = pk;
|
|
71
|
+
_followingClientIdFallback.value = null;
|
|
72
|
+
} else {
|
|
73
|
+
if (_followingClientIdFallback.value === cid) return;
|
|
74
|
+
_followingPublicKey.value = null;
|
|
75
|
+
_followingClientIdFallback.value = cid;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
37
78
|
attachInteractionBreaker();
|
|
38
79
|
}
|
|
39
80
|
function unfollow() {
|
|
40
|
-
if (
|
|
41
|
-
|
|
81
|
+
if (_followingPublicKey.value == null && _followingClientIdFallback.value == null) return;
|
|
82
|
+
_followingPublicKey.value = null;
|
|
83
|
+
_followingClientIdFallback.value = null;
|
|
42
84
|
detachInteraction();
|
|
43
85
|
options.onBreak?.();
|
|
44
86
|
}
|
|
@@ -71,5 +113,12 @@ export function useAAFollowPeer(options = {}) {
|
|
|
71
113
|
}
|
|
72
114
|
});
|
|
73
115
|
onScopeDispose(detachInteraction);
|
|
74
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
follow,
|
|
118
|
+
unfollow,
|
|
119
|
+
followingClientId,
|
|
120
|
+
followingPublicKey,
|
|
121
|
+
followingUser,
|
|
122
|
+
isFollowing
|
|
123
|
+
};
|
|
75
124
|
}
|
|
@@ -6,7 +6,7 @@ import type { MaybeRefOrGetter } from 'vue';
|
|
|
6
6
|
* scoped container as `peer:viewport:<scope>` ephemeral state.
|
|
7
7
|
*
|
|
8
8
|
* Use case: render "Janis is reading the API section" overlays in long docs.
|
|
9
|
-
* Pair with `<APresenceCursors>` for a complete
|
|
9
|
+
* Pair with `<APresenceCursors>` for a complete presence layer.
|
|
10
10
|
*/
|
|
11
11
|
export interface PeerViewport {
|
|
12
12
|
clientId: number;
|
|
@@ -24,6 +24,8 @@ export function useAbracadabraAuth() {
|
|
|
24
24
|
confirmPasswordReset: abra.confirmPasswordReset,
|
|
25
25
|
changePassword: abra.changePassword,
|
|
26
26
|
setPassword: abra.setPassword,
|
|
27
|
+
hasPassword: abra.hasPassword,
|
|
28
|
+
refreshHasPassword: abra.refreshHasPassword,
|
|
27
29
|
recoverIdentity: abra.recoverIdentity,
|
|
28
30
|
unblockConnection: abra.unblockConnection,
|
|
29
31
|
setUserName: abra.setUserName,
|
|
@@ -1,33 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
declare function request(): Promise<void>;
|
|
16
|
-
declare function confirm(token: string): Promise<void>;
|
|
1
|
+
/** Known error codes raised by `useEmailVerification()` actions. */
|
|
2
|
+
export type EmailVerificationErrorCode = 'rate_limited' | 'token_expired' | 'token_invalid' | 'verification_disabled' | 'already_verified' | 'email_not_set' | 'unauthorized' | 'forbidden' | 'network_error' | 'server_error' | 'unknown';
|
|
3
|
+
export interface EmailVerificationError {
|
|
4
|
+
code: EmailVerificationErrorCode;
|
|
5
|
+
message: string;
|
|
6
|
+
status?: number;
|
|
7
|
+
retryAfterSeconds?: number;
|
|
8
|
+
}
|
|
9
|
+
export type EmailVerificationAction = 'request' | 'confirm';
|
|
10
|
+
export interface EmailVerificationResult {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
error?: EmailVerificationError;
|
|
13
|
+
}
|
|
14
|
+
declare function classifyError(err: unknown): EmailVerificationError;
|
|
17
15
|
export declare function useEmailVerification(): {
|
|
18
|
-
/** True after a successful `confirm()` in
|
|
16
|
+
/** True after a successful `confirm()` in this session. */
|
|
19
17
|
verified: import("vue").Ref<boolean, boolean>;
|
|
20
|
-
/** Whether a `request()` is in flight. */
|
|
21
|
-
isRequesting: import("vue").Ref<boolean, boolean>;
|
|
22
|
-
/** Whether a `confirm()` is in flight. */
|
|
23
|
-
isConfirming: import("vue").Ref<boolean, boolean>;
|
|
24
|
-
/** Last error message, or null. */
|
|
25
|
-
error: import("vue").Ref<string | null, string | null>;
|
|
26
18
|
/** Epoch ms of the last successful `request()`, or null. */
|
|
27
19
|
lastSentAt: import("vue").Ref<number | null, number | null>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
20
|
+
/** Currently-running action, or null. */
|
|
21
|
+
busy: import("vue").Ref<EmailVerificationAction | null, EmailVerificationAction | null>;
|
|
22
|
+
/** Most recent typed error, or null. Cleared at the start of every action. */
|
|
23
|
+
error: import("vue").Ref<{
|
|
24
|
+
code: EmailVerificationErrorCode;
|
|
25
|
+
message: string;
|
|
26
|
+
status?: number | undefined;
|
|
27
|
+
retryAfterSeconds?: number | undefined;
|
|
28
|
+
} | null, EmailVerificationError | {
|
|
29
|
+
code: EmailVerificationErrorCode;
|
|
30
|
+
message: string;
|
|
31
|
+
status?: number | undefined;
|
|
32
|
+
retryAfterSeconds?: number | undefined;
|
|
33
|
+
} | null>;
|
|
34
|
+
/** Most recent successful action, or null. */
|
|
35
|
+
lastSuccess: import("vue").Ref<EmailVerificationAction | null, EmailVerificationAction | null>;
|
|
36
|
+
/** Wipe `error` without re-running anything. */
|
|
37
|
+
clearError: () => void;
|
|
38
|
+
/** Send a verification email to the current user. Auth required. */
|
|
39
|
+
request: () => Promise<EmailVerificationResult>;
|
|
30
40
|
/** Confirm a verification token from the email link. No auth required. */
|
|
31
|
-
confirm:
|
|
41
|
+
confirm: (opts: {
|
|
42
|
+
token: string;
|
|
43
|
+
}) => Promise<EmailVerificationResult>;
|
|
44
|
+
/** Internal helper exposed for testing. */
|
|
45
|
+
_classifyError: typeof classifyError;
|
|
32
46
|
};
|
|
33
47
|
export {};
|
|
@@ -1,58 +1,110 @@
|
|
|
1
1
|
import { ref } from "vue";
|
|
2
|
-
import { useAbracadabra } from "
|
|
2
|
+
import { useAbracadabra } from "./useAbracadabra.js";
|
|
3
3
|
const verified = ref(false);
|
|
4
|
-
const isRequesting = ref(false);
|
|
5
|
-
const isConfirming = ref(false);
|
|
6
|
-
const error = ref(null);
|
|
7
4
|
const lastSentAt = ref(null);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
error.value = e instanceof Error ? e.message : "Failed to send verification email";
|
|
19
|
-
throw e;
|
|
20
|
-
} finally {
|
|
21
|
-
isRequesting.value = false;
|
|
22
|
-
}
|
|
5
|
+
const busy = ref(null);
|
|
6
|
+
const error = ref(null);
|
|
7
|
+
const lastSuccess = ref(null);
|
|
8
|
+
function extractServerMessage(raw) {
|
|
9
|
+
const trailing = raw.replace(/ \(\d+\)$/, "").trim();
|
|
10
|
+
const colon = trailing.indexOf(": ");
|
|
11
|
+
if (colon === -1) return trailing;
|
|
12
|
+
const head = trailing.slice(0, colon);
|
|
13
|
+
if (/^[A-Z]+ \//.test(head)) return trailing.slice(colon + 2).trim();
|
|
14
|
+
return trailing;
|
|
23
15
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
16
|
+
function classifyError(err) {
|
|
17
|
+
if (err instanceof TypeError && /fetch|network/i.test(err.message)) {
|
|
18
|
+
return { code: "network_error", message: err.message };
|
|
19
|
+
}
|
|
20
|
+
if (!(err instanceof Error)) {
|
|
21
|
+
return { code: "unknown", message: String(err) };
|
|
22
|
+
}
|
|
23
|
+
const status = err.status;
|
|
24
|
+
const serverMsg = extractServerMessage(err.message);
|
|
25
|
+
const lower = serverMsg.toLowerCase();
|
|
26
|
+
const retryMatch = lower.match(/retry(?:[-\s]?after)?[:\s]+(\d+)/);
|
|
27
|
+
const retryAfterSeconds = retryMatch ? Number(retryMatch[1]) : void 0;
|
|
28
|
+
if (status === 429) {
|
|
29
|
+
return { code: "rate_limited", message: serverMsg, status, retryAfterSeconds };
|
|
30
|
+
}
|
|
31
|
+
if (status === 410 || /token (?:expired|invalid)/.test(lower)) {
|
|
32
|
+
return {
|
|
33
|
+
code: lower.includes("expired") ? "token_expired" : "token_invalid",
|
|
34
|
+
message: serverMsg,
|
|
35
|
+
status
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (status === 404 || /verification (?:is )?disabled/.test(lower)) {
|
|
39
|
+
return { code: "verification_disabled", message: serverMsg, status };
|
|
40
|
+
}
|
|
41
|
+
if (lower.includes("already verified") || lower.includes("already_verified")) {
|
|
42
|
+
return { code: "already_verified", message: serverMsg, status };
|
|
39
43
|
}
|
|
44
|
+
if (lower.includes("no email") || lower.includes("email not set") || lower.includes("email_not_set")) {
|
|
45
|
+
return { code: "email_not_set", message: serverMsg, status };
|
|
46
|
+
}
|
|
47
|
+
if (status === 401) return { code: "unauthorized", message: serverMsg, status };
|
|
48
|
+
if (status === 403) return { code: "forbidden", message: serverMsg, status };
|
|
49
|
+
if (typeof status === "number" && status >= 500) {
|
|
50
|
+
return { code: "server_error", message: serverMsg, status };
|
|
51
|
+
}
|
|
52
|
+
return { code: "unknown", message: serverMsg, status };
|
|
40
53
|
}
|
|
41
54
|
export function useEmailVerification() {
|
|
55
|
+
const abra = useAbracadabra();
|
|
56
|
+
function clearError() {
|
|
57
|
+
error.value = null;
|
|
58
|
+
}
|
|
59
|
+
async function run(action, fn) {
|
|
60
|
+
const client = abra.client.value;
|
|
61
|
+
if (!client) {
|
|
62
|
+
const err = { code: "network_error", message: "Not connected" };
|
|
63
|
+
error.value = err;
|
|
64
|
+
return { ok: false, error: err };
|
|
65
|
+
}
|
|
66
|
+
busy.value = action;
|
|
67
|
+
error.value = null;
|
|
68
|
+
try {
|
|
69
|
+
await fn(client);
|
|
70
|
+
lastSuccess.value = action;
|
|
71
|
+
return { ok: true };
|
|
72
|
+
} catch (e) {
|
|
73
|
+
const err = classifyError(e);
|
|
74
|
+
error.value = err;
|
|
75
|
+
return { ok: false, error: err };
|
|
76
|
+
} finally {
|
|
77
|
+
busy.value = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function request() {
|
|
81
|
+
const res = await run("request", (client) => client.requestEmailVerification());
|
|
82
|
+
if (res.ok) lastSentAt.value = Date.now();
|
|
83
|
+
return res;
|
|
84
|
+
}
|
|
85
|
+
async function confirm(opts) {
|
|
86
|
+
const res = await run("confirm", (client) => client.confirmEmailVerification(opts.token));
|
|
87
|
+
if (res.ok) verified.value = true;
|
|
88
|
+
return res;
|
|
89
|
+
}
|
|
42
90
|
return {
|
|
43
|
-
/** True after a successful `confirm()` in
|
|
91
|
+
/** True after a successful `confirm()` in this session. */
|
|
44
92
|
verified,
|
|
45
|
-
/** Whether a `request()` is in flight. */
|
|
46
|
-
isRequesting,
|
|
47
|
-
/** Whether a `confirm()` is in flight. */
|
|
48
|
-
isConfirming,
|
|
49
|
-
/** Last error message, or null. */
|
|
50
|
-
error,
|
|
51
93
|
/** Epoch ms of the last successful `request()`, or null. */
|
|
52
94
|
lastSentAt,
|
|
53
|
-
/**
|
|
95
|
+
/** Currently-running action, or null. */
|
|
96
|
+
busy,
|
|
97
|
+
/** Most recent typed error, or null. Cleared at the start of every action. */
|
|
98
|
+
error,
|
|
99
|
+
/** Most recent successful action, or null. */
|
|
100
|
+
lastSuccess,
|
|
101
|
+
/** Wipe `error` without re-running anything. */
|
|
102
|
+
clearError,
|
|
103
|
+
/** Send a verification email to the current user. Auth required. */
|
|
54
104
|
request,
|
|
55
105
|
/** Confirm a verification token from the email link. No auth required. */
|
|
56
|
-
confirm
|
|
106
|
+
confirm,
|
|
107
|
+
/** Internal helper exposed for testing. */
|
|
108
|
+
_classifyError: classifyError
|
|
57
109
|
};
|
|
58
110
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Known error codes raised by `usePasswordAuth()` actions. */
|
|
2
|
+
export type PasswordAuthErrorCode = 'rate_limited' | 'locked_out' | 'password_too_short' | 'invalid_credentials' | 'token_expired' | 'token_invalid' | 'password_already_set' | 'email_taken' | 'username_taken' | 'password_login_disabled' | 'password_reset_disabled' | 'invite_required' | 'invite_invalid' | 'network_error' | 'forbidden' | 'unauthorized' | 'server_error' | 'unknown';
|
|
3
|
+
export interface PasswordAuthError {
|
|
4
|
+
code: PasswordAuthErrorCode;
|
|
5
|
+
message: string;
|
|
6
|
+
/** HTTP status code, when the error came from the REST API. */
|
|
7
|
+
status?: number;
|
|
8
|
+
/** Seconds the caller should wait before retrying — only set for `rate_limited` / `locked_out`. */
|
|
9
|
+
retryAfterSeconds?: number;
|
|
10
|
+
}
|
|
11
|
+
export type PasswordAuthAction = 'login' | 'register' | 'reset-request' | 'reset-confirm' | 'change' | 'set';
|
|
12
|
+
export interface PasswordAuthResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
error?: PasswordAuthError;
|
|
15
|
+
}
|
|
16
|
+
declare function classifyError(err: unknown): PasswordAuthError;
|
|
17
|
+
export declare function usePasswordAuth(): {
|
|
18
|
+
busy: import("vue").Ref<PasswordAuthAction | null, PasswordAuthAction | null>;
|
|
19
|
+
error: import("vue").Ref<{
|
|
20
|
+
code: PasswordAuthErrorCode;
|
|
21
|
+
message: string;
|
|
22
|
+
status?: number
|
|
23
|
+
/** Seconds the caller should wait before retrying — only set for `rate_limited` / `locked_out`. */
|
|
24
|
+
| undefined;
|
|
25
|
+
retryAfterSeconds?: number | undefined;
|
|
26
|
+
} | null, PasswordAuthError | {
|
|
27
|
+
code: PasswordAuthErrorCode;
|
|
28
|
+
message: string;
|
|
29
|
+
status?: number
|
|
30
|
+
/** Seconds the caller should wait before retrying — only set for `rate_limited` / `locked_out`. */
|
|
31
|
+
| undefined;
|
|
32
|
+
retryAfterSeconds?: number | undefined;
|
|
33
|
+
} | null>;
|
|
34
|
+
lastSuccess: import("vue").Ref<PasswordAuthAction | null, PasswordAuthAction | null>;
|
|
35
|
+
clearError: () => void;
|
|
36
|
+
login: (opts: {
|
|
37
|
+
username: string;
|
|
38
|
+
password: string;
|
|
39
|
+
}) => Promise<PasswordAuthResult>;
|
|
40
|
+
register: (opts: {
|
|
41
|
+
username: string;
|
|
42
|
+
password: string;
|
|
43
|
+
email?: string;
|
|
44
|
+
displayName?: string;
|
|
45
|
+
inviteCode?: string;
|
|
46
|
+
}) => Promise<PasswordAuthResult>;
|
|
47
|
+
requestReset: (opts: {
|
|
48
|
+
identifier: string;
|
|
49
|
+
}) => Promise<PasswordAuthResult>;
|
|
50
|
+
confirmReset: (opts: {
|
|
51
|
+
token: string;
|
|
52
|
+
newPassword: string;
|
|
53
|
+
}) => Promise<PasswordAuthResult>;
|
|
54
|
+
changePassword: (opts: {
|
|
55
|
+
currentPassword: string;
|
|
56
|
+
newPassword: string;
|
|
57
|
+
}) => Promise<PasswordAuthResult>;
|
|
58
|
+
setPassword: (opts: {
|
|
59
|
+
password: string;
|
|
60
|
+
}) => Promise<PasswordAuthResult>;
|
|
61
|
+
/** Internal helper exposed for testing — classify any error to its code. */
|
|
62
|
+
_classifyError: typeof classifyError;
|
|
63
|
+
};
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import { useAbracadabraAuth } from "./useAbracadabraAuth.js";
|
|
3
|
+
function extractServerMessage(raw) {
|
|
4
|
+
const trailing = raw.replace(/ \(\d+\)$/, "").trim();
|
|
5
|
+
const colon = trailing.indexOf(": ");
|
|
6
|
+
if (colon === -1) return trailing;
|
|
7
|
+
const head = trailing.slice(0, colon);
|
|
8
|
+
if (/^[A-Z]+ \//.test(head)) return trailing.slice(colon + 2).trim();
|
|
9
|
+
return trailing;
|
|
10
|
+
}
|
|
11
|
+
function classifyError(err) {
|
|
12
|
+
if (err instanceof TypeError && /fetch|network/i.test(err.message)) {
|
|
13
|
+
return { code: "network_error", message: err.message };
|
|
14
|
+
}
|
|
15
|
+
if (!(err instanceof Error)) {
|
|
16
|
+
return { code: "unknown", message: String(err) };
|
|
17
|
+
}
|
|
18
|
+
const status = err.status;
|
|
19
|
+
const serverMsg = extractServerMessage(err.message);
|
|
20
|
+
const lower = serverMsg.toLowerCase();
|
|
21
|
+
const retryMatch = lower.match(/retry(?:[-\s]?after)?[:\s]+(\d+)/);
|
|
22
|
+
const retryAfterSeconds = retryMatch ? Number(retryMatch[1]) : void 0;
|
|
23
|
+
if (status === 423) {
|
|
24
|
+
return { code: "locked_out", message: serverMsg, status, retryAfterSeconds };
|
|
25
|
+
}
|
|
26
|
+
if (status === 429) {
|
|
27
|
+
return { code: "rate_limited", message: serverMsg, status, retryAfterSeconds };
|
|
28
|
+
}
|
|
29
|
+
if (status === 410 || /token (?:expired|invalid)/.test(lower)) {
|
|
30
|
+
return {
|
|
31
|
+
code: lower.includes("expired") ? "token_expired" : "token_invalid",
|
|
32
|
+
message: serverMsg,
|
|
33
|
+
status
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (lower.includes("password too short")) {
|
|
37
|
+
return { code: "password_too_short", message: serverMsg, status };
|
|
38
|
+
}
|
|
39
|
+
if (lower.includes("password_already_set")) {
|
|
40
|
+
return { code: "password_already_set", message: serverMsg, status };
|
|
41
|
+
}
|
|
42
|
+
if (lower.includes("password login is disabled")) {
|
|
43
|
+
return { code: "password_login_disabled", message: serverMsg, status };
|
|
44
|
+
}
|
|
45
|
+
if (lower.includes("password reset disabled") || lower.includes("password reset is disabled")) {
|
|
46
|
+
return { code: "password_reset_disabled", message: serverMsg, status };
|
|
47
|
+
}
|
|
48
|
+
if (lower.includes("username") && (lower.includes("taken") || lower.includes("exists") || lower.includes("in use"))) {
|
|
49
|
+
return { code: "username_taken", message: serverMsg, status };
|
|
50
|
+
}
|
|
51
|
+
if (lower.includes("email") && (lower.includes("taken") || lower.includes("exists") || lower.includes("in use"))) {
|
|
52
|
+
return { code: "email_taken", message: serverMsg, status };
|
|
53
|
+
}
|
|
54
|
+
if (lower.includes("invite required") || lower.includes("invite is required")) {
|
|
55
|
+
return { code: "invite_required", message: serverMsg, status };
|
|
56
|
+
}
|
|
57
|
+
if (lower.includes("invite") && (lower.includes("invalid") || lower.includes("expired") || lower.includes("used"))) {
|
|
58
|
+
return { code: "invite_invalid", message: serverMsg, status };
|
|
59
|
+
}
|
|
60
|
+
if (status === 401 || lower.includes("unauthorized") || lower.includes("invalid credentials") || lower.includes("bad password")) {
|
|
61
|
+
return { code: status === 401 ? "unauthorized" : "invalid_credentials", message: serverMsg, status };
|
|
62
|
+
}
|
|
63
|
+
if (status === 403) {
|
|
64
|
+
return { code: "forbidden", message: serverMsg, status };
|
|
65
|
+
}
|
|
66
|
+
if (typeof status === "number" && status >= 500) {
|
|
67
|
+
return { code: "server_error", message: serverMsg, status };
|
|
68
|
+
}
|
|
69
|
+
return { code: "unknown", message: serverMsg, status };
|
|
70
|
+
}
|
|
71
|
+
export function usePasswordAuth() {
|
|
72
|
+
const auth = useAbracadabraAuth();
|
|
73
|
+
const busy = ref(null);
|
|
74
|
+
const error = ref(null);
|
|
75
|
+
const lastSuccess = ref(null);
|
|
76
|
+
function clearError() {
|
|
77
|
+
error.value = null;
|
|
78
|
+
}
|
|
79
|
+
async function run(action, fn) {
|
|
80
|
+
busy.value = action;
|
|
81
|
+
error.value = null;
|
|
82
|
+
try {
|
|
83
|
+
await fn();
|
|
84
|
+
lastSuccess.value = action;
|
|
85
|
+
return { ok: true };
|
|
86
|
+
} catch (e) {
|
|
87
|
+
const err = classifyError(e);
|
|
88
|
+
error.value = err;
|
|
89
|
+
return { ok: false, error: err };
|
|
90
|
+
} finally {
|
|
91
|
+
busy.value = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function login(opts) {
|
|
95
|
+
return run("login", () => auth.loginWithPassword(opts));
|
|
96
|
+
}
|
|
97
|
+
async function register(opts) {
|
|
98
|
+
return run("register", () => auth.registerWithPassword(opts));
|
|
99
|
+
}
|
|
100
|
+
async function requestReset(opts) {
|
|
101
|
+
return run("reset-request", () => auth.requestPasswordReset(opts));
|
|
102
|
+
}
|
|
103
|
+
async function confirmReset(opts) {
|
|
104
|
+
return run("reset-confirm", () => auth.confirmPasswordReset(opts));
|
|
105
|
+
}
|
|
106
|
+
async function changePassword(opts) {
|
|
107
|
+
return run("change", () => auth.changePassword(opts));
|
|
108
|
+
}
|
|
109
|
+
async function setPassword(opts) {
|
|
110
|
+
return run("set", () => auth.setPassword(opts.password));
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
busy,
|
|
114
|
+
error,
|
|
115
|
+
lastSuccess,
|
|
116
|
+
clearError,
|
|
117
|
+
login,
|
|
118
|
+
register,
|
|
119
|
+
requestReset,
|
|
120
|
+
confirmReset,
|
|
121
|
+
changePassword,
|
|
122
|
+
setPassword,
|
|
123
|
+
/** Internal helper exposed for testing — classify any error to its code. */
|
|
124
|
+
_classifyError: classifyError
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* const history = useTiptapHistory(editorRef)
|
|
13
13
|
* <UButton :disabled="!history.canUndo.value" @click="history.undo" />
|
|
14
14
|
*/
|
|
15
|
-
import { type
|
|
15
|
+
import { type MaybeRefOrGetter } from 'vue';
|
|
16
16
|
import type { Editor } from '@tiptap/vue-3';
|
|
17
|
-
export declare function useTiptapHistory(editor:
|
|
17
|
+
export declare function useTiptapHistory(editor: MaybeRefOrGetter<Editor | null | undefined>): {
|
|
18
18
|
canUndo: import("vue").ComputedRef<boolean>;
|
|
19
19
|
canRedo: import("vue").ComputedRef<boolean>;
|
|
20
20
|
undo: () => void;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { computed,
|
|
1
|
+
import { computed, toValue } from "vue";
|
|
2
2
|
export function useTiptapHistory(editor) {
|
|
3
|
-
const canUndo = computed(() => !!
|
|
4
|
-
const canRedo = computed(() => !!
|
|
3
|
+
const canUndo = computed(() => !!toValue(editor)?.can?.().undo?.());
|
|
4
|
+
const canRedo = computed(() => !!toValue(editor)?.can?.().redo?.());
|
|
5
5
|
function undo() {
|
|
6
|
-
|
|
6
|
+
toValue(editor)?.commands?.undo?.();
|
|
7
7
|
}
|
|
8
8
|
function redo() {
|
|
9
|
-
|
|
9
|
+
toValue(editor)?.commands?.redo?.();
|
|
10
10
|
}
|
|
11
11
|
return { canUndo, canRedo, undo, redo };
|
|
12
12
|
}
|
|
@@ -140,12 +140,23 @@ function onDateTimeRange(range) {
|
|
|
140
140
|
[endKey.value]: `${dvToIso(range.end)}T${et}:00.000Z`
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
|
-
function
|
|
143
|
+
function asSingleTime(t2) {
|
|
144
|
+
if (t2 && typeof t2 === "object" && "hour" in t2 && "minute" in t2) {
|
|
145
|
+
const v = t2;
|
|
146
|
+
if (typeof v.hour === "number" && typeof v.minute === "number") {
|
|
147
|
+
return { hour: v.hour, minute: v.minute };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function onStartTime(raw) {
|
|
153
|
+
const t2 = asSingleTime(raw);
|
|
144
154
|
if (!t2) return;
|
|
145
155
|
const date = getStr(startKey.value).slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
146
156
|
patch({ [startKey.value]: `${date}T${timeToHHMM(t2)}:00.000Z` });
|
|
147
157
|
}
|
|
148
|
-
function onEndTime(
|
|
158
|
+
function onEndTime(raw) {
|
|
159
|
+
const t2 = asSingleTime(raw);
|
|
149
160
|
if (!t2) return;
|
|
150
161
|
const date = getStr(endKey.value).slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
151
162
|
patch({ [endKey.value]: `${date}T${timeToHHMM(t2)}:00.000Z` });
|
|
@@ -257,15 +268,18 @@ function onSingleDatetimeDate(d) {
|
|
|
257
268
|
const time = getStr(metaKey.value).slice(11, 16) || "00:00";
|
|
258
269
|
patch({ [metaKey.value]: `${date}T${time}:00.000Z` });
|
|
259
270
|
}
|
|
260
|
-
function onSingleTime(
|
|
271
|
+
function onSingleTime(raw) {
|
|
272
|
+
const t2 = asSingleTime(raw);
|
|
261
273
|
if (!t2) return;
|
|
262
274
|
patch({ [metaKey.value]: timeToHHMM(t2) });
|
|
263
275
|
}
|
|
264
|
-
function onTimeRangeStart(
|
|
276
|
+
function onTimeRangeStart(raw) {
|
|
277
|
+
const t2 = asSingleTime(raw);
|
|
265
278
|
if (!t2) return;
|
|
266
279
|
patch({ [startKey.value]: timeToHHMM(t2) });
|
|
267
280
|
}
|
|
268
|
-
function onTimeRangeEnd(
|
|
281
|
+
function onTimeRangeEnd(raw) {
|
|
282
|
+
const t2 = asSingleTime(raw);
|
|
269
283
|
if (!t2) return;
|
|
270
284
|
patch({ [endKey.value]: timeToHHMM(t2) });
|
|
271
285
|
}
|
|
@@ -1089,7 +1103,10 @@ function removeOption(opt) {
|
|
|
1089
1103
|
<UInputTime
|
|
1090
1104
|
size="sm"
|
|
1091
1105
|
:model-value="isoToTime(getStr(metaKey).slice(11, 16) || '00:00')"
|
|
1092
|
-
@update:model-value="(
|
|
1106
|
+
@update:model-value="(raw) => {
|
|
1107
|
+
const t2 = asSingleTime(raw);
|
|
1108
|
+
if (t2) patch({ [metaKey]: `${getStr(metaKey).slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}T${timeToHHMM(t2)}:00.000Z` });
|
|
1109
|
+
}"
|
|
1093
1110
|
/>
|
|
1094
1111
|
</div>
|
|
1095
1112
|
</template>
|