@abraca/nuxt 2.0.0 → 2.0.3
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/AEditor.vue +5 -0
- 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/ASubPageList.d.vue.ts +66 -0
- package/dist/runtime/components/ASubPageList.vue +147 -0
- package/dist/runtime/components/ASubPageList.vue.d.ts +66 -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/renderers/calendar/ACalendarToolbar.d.vue.ts +4 -4
- package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +4 -4
- package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/media/MediaTransportBar.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/useEditorSuggestions.js +2 -1
- 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/svg-embed.d.ts +23 -0
- package/dist/runtime/extensions/svg-embed.js +33 -0
- package/dist/runtime/extensions/views/MetaFieldView.vue +23 -6
- package/dist/runtime/extensions/views/SvgEmbedView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/SvgEmbedView.vue +120 -0
- package/dist/runtime/extensions/views/SvgEmbedView.vue.d.ts +4 -0
- package/dist/runtime/plugin-abracadabra.client.js +58 -9
- package/dist/runtime/plugin-abracadabra.server.js +2 -0
- package/dist/runtime/plugins/core.plugin.js +8 -4
- package/dist/runtime/server/plugins/abracadabra-service.js +102 -13
- package/dist/runtime/types.d.ts +11 -0
- package/dist/runtime/utils/awareRingStyle.js +1 -1
- package/dist/runtime/utils/sanitizeSvg.d.ts +19 -0
- package/dist/runtime/utils/sanitizeSvg.js +87 -0
- package/package.json +7 -8
- 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
|
@@ -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
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Node } from '@tiptap/core';
|
|
2
|
+
declare module '@tiptap/core' {
|
|
3
|
+
interface Commands<ReturnType> {
|
|
4
|
+
svgEmbed: {
|
|
5
|
+
insertSvgEmbed: (attrs: {
|
|
6
|
+
svg?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
width?: string | null;
|
|
9
|
+
height?: string | null;
|
|
10
|
+
}) => ReturnType;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* SVG embed extension.
|
|
16
|
+
*
|
|
17
|
+
* The extension itself has no peer dep. Sanitization happens inside the
|
|
18
|
+
* NodeView via `runtime/utils/sanitizeSvg.ts`, which try-imports DOMPurify
|
|
19
|
+
* and falls back to a strict built-in allowlist when DOMPurify isn't
|
|
20
|
+
* installed. Apps that need richer SVG features (animations, CSS) should
|
|
21
|
+
* `pnpm add dompurify`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SvgEmbed: Node<any, any>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import { VueNodeViewRenderer } from "@tiptap/vue-3";
|
|
3
|
+
import SvgEmbedView from "./views/SvgEmbedView.vue";
|
|
4
|
+
export const SvgEmbed = Node.create({
|
|
5
|
+
name: "svgEmbed",
|
|
6
|
+
group: "block",
|
|
7
|
+
atom: true,
|
|
8
|
+
draggable: true,
|
|
9
|
+
addAttributes() {
|
|
10
|
+
return {
|
|
11
|
+
svg: { default: "" },
|
|
12
|
+
title: { default: "" },
|
|
13
|
+
width: { default: null },
|
|
14
|
+
height: { default: null }
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
parseHTML() {
|
|
18
|
+
return [{ tag: 'div[data-type="svg-embed"]' }];
|
|
19
|
+
},
|
|
20
|
+
renderHTML({ HTMLAttributes }) {
|
|
21
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-type": "svg-embed" })];
|
|
22
|
+
},
|
|
23
|
+
addNodeView() {
|
|
24
|
+
return VueNodeViewRenderer(SvgEmbedView);
|
|
25
|
+
},
|
|
26
|
+
addCommands() {
|
|
27
|
+
return {
|
|
28
|
+
insertSvgEmbed: (attrs) => ({ commands }) => {
|
|
29
|
+
return commands.insertContent({ type: this.name, attrs });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
});
|
|
@@ -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>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NodeViewProps } from '@tiptap/vue-3';
|
|
2
|
+
declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
3
|
+
declare const _default: typeof __VLS_export;
|
|
4
|
+
export default _default;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch, watchEffect } from "vue";
|
|
3
|
+
import { NodeViewWrapper } from "@tiptap/vue-3";
|
|
4
|
+
import { sanitizeSvg } from "../../utils/sanitizeSvg";
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
decorations: { type: Array, required: true },
|
|
7
|
+
selected: { type: Boolean, required: true },
|
|
8
|
+
updateAttributes: { type: Function, required: true },
|
|
9
|
+
deleteNode: { type: Function, required: true },
|
|
10
|
+
node: { type: null, required: true },
|
|
11
|
+
view: { type: null, required: true },
|
|
12
|
+
getPos: { type: null, required: true },
|
|
13
|
+
innerDecorations: { type: null, required: true },
|
|
14
|
+
editor: { type: Object, required: true },
|
|
15
|
+
extension: { type: Object, required: true },
|
|
16
|
+
HTMLAttributes: { type: Object, required: true }
|
|
17
|
+
});
|
|
18
|
+
const rawSvg = computed(() => props.node.attrs.svg || "");
|
|
19
|
+
const title = computed(() => props.node.attrs.title || "");
|
|
20
|
+
const hasSvg = computed(() => !!rawSvg.value);
|
|
21
|
+
const sanitized = ref("");
|
|
22
|
+
const isDragOver = ref(false);
|
|
23
|
+
watchEffect(async () => {
|
|
24
|
+
sanitized.value = await sanitizeSvg(rawSvg.value);
|
|
25
|
+
});
|
|
26
|
+
const containerStyle = computed(() => {
|
|
27
|
+
const s = {};
|
|
28
|
+
if (props.node.attrs.width) s.width = String(props.node.attrs.width);
|
|
29
|
+
if (props.node.attrs.height) s.height = String(props.node.attrs.height);
|
|
30
|
+
return s;
|
|
31
|
+
});
|
|
32
|
+
function isSvgFile(file) {
|
|
33
|
+
return file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg");
|
|
34
|
+
}
|
|
35
|
+
async function loadSvgFile(file) {
|
|
36
|
+
const text = await file.text();
|
|
37
|
+
if (!text.includes("<svg")) return;
|
|
38
|
+
props.updateAttributes({
|
|
39
|
+
svg: text,
|
|
40
|
+
title: props.node.attrs.title || file.name.replace(/\.svg$/i, "")
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function pickSvgFile() {
|
|
44
|
+
const input = document.createElement("input");
|
|
45
|
+
input.type = "file";
|
|
46
|
+
input.accept = ".svg,image/svg+xml";
|
|
47
|
+
input.onchange = () => {
|
|
48
|
+
const file = input.files?.[0];
|
|
49
|
+
if (file) loadSvgFile(file);
|
|
50
|
+
};
|
|
51
|
+
input.click();
|
|
52
|
+
}
|
|
53
|
+
function onDrop(e) {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
isDragOver.value = false;
|
|
57
|
+
const file = Array.from(e.dataTransfer?.files ?? []).find(isSvgFile);
|
|
58
|
+
if (file) loadSvgFile(file);
|
|
59
|
+
}
|
|
60
|
+
function onDragOver(e) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
isDragOver.value = true;
|
|
64
|
+
}
|
|
65
|
+
function onDragLeave() {
|
|
66
|
+
isDragOver.value = false;
|
|
67
|
+
}
|
|
68
|
+
const placeholderText = computed(() => {
|
|
69
|
+
if (isDragOver.value) return "Drop SVG file here";
|
|
70
|
+
if (hasSvg.value && !sanitized.value) return "SVG removed by sanitizer";
|
|
71
|
+
return "Click or drop an SVG file";
|
|
72
|
+
});
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<NodeViewWrapper
|
|
77
|
+
class="svg-embed-wrapper my-3"
|
|
78
|
+
data-type="svg-embed"
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
contenteditable="false"
|
|
82
|
+
data-drag-handle
|
|
83
|
+
class="border border-(--ui-border) rounded-md overflow-hidden transition-colors"
|
|
84
|
+
:class="{
|
|
85
|
+
'border-(--ui-primary) bg-(--ui-primary)/5': isDragOver,
|
|
86
|
+
'border-(--ui-primary)': props.selected && !isDragOver
|
|
87
|
+
}"
|
|
88
|
+
@drop="onDrop"
|
|
89
|
+
@dragover="onDragOver"
|
|
90
|
+
@dragleave="onDragLeave"
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
v-if="title && sanitized"
|
|
94
|
+
class="px-3 py-1.5 text-xs font-medium text-(--ui-text-dimmed) border-b border-(--ui-border) bg-(--ui-bg-elevated)"
|
|
95
|
+
>
|
|
96
|
+
{{ title }}
|
|
97
|
+
</div>
|
|
98
|
+
<div
|
|
99
|
+
v-if="sanitized"
|
|
100
|
+
class="flex items-center justify-center p-2 [&_svg]:max-w-full [&_svg]:h-auto"
|
|
101
|
+
:style="containerStyle"
|
|
102
|
+
v-html="sanitized"
|
|
103
|
+
/>
|
|
104
|
+
<div
|
|
105
|
+
v-else
|
|
106
|
+
class="flex flex-col items-center justify-center gap-2 px-6 py-10 cursor-pointer text-(--ui-text-dimmed) hover:bg-(--ui-bg-elevated)/40 transition-colors"
|
|
107
|
+
role="button"
|
|
108
|
+
tabindex="0"
|
|
109
|
+
@click="pickSvgFile"
|
|
110
|
+
@keydown.enter="pickSvgFile"
|
|
111
|
+
>
|
|
112
|
+
<UIcon
|
|
113
|
+
name="i-lucide-image"
|
|
114
|
+
class="size-6"
|
|
115
|
+
/>
|
|
116
|
+
<span class="text-sm">{{ placeholderText }}</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</NodeViewWrapper>
|
|
120
|
+
</template>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NodeViewProps } from '@tiptap/vue-3';
|
|
2
|
+
declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
3
|
+
declare const _default: typeof __VLS_export;
|
|
4
|
+
export default _default;
|