@lenne.tech/nuxt-extensions 1.0.1 → 1.1.0
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 +2 -1
- package/dist/module.mjs +16 -0
- package/dist/runtime/composables/index.d.ts +1 -0
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/use-lt-auth-client.d.ts +8 -0
- package/dist/runtime/composables/use-lt-auth-client.js +26 -2
- package/dist/runtime/composables/use-lt-error-translation.d.ts +33 -0
- package/dist/runtime/composables/use-lt-error-translation.js +133 -0
- package/dist/runtime/lib/auth-client.js +2 -2
- package/dist/runtime/lib/auth-state.d.ts +9 -0
- package/dist/runtime/lib/auth-state.js +17 -1
- package/dist/runtime/locales/de.json +4 -0
- package/dist/runtime/locales/en.json +4 -0
- package/dist/runtime/plugins/auth-interceptor.client.js +5 -3
- package/dist/runtime/plugins/error-translation.client.d.ts +8 -0
- package/dist/runtime/plugins/error-translation.client.js +17 -0
- package/dist/runtime/testing/index.d.ts +21 -0
- package/dist/runtime/testing/index.js +17 -0
- package/dist/runtime/testing/playwright-helpers.d.ts +296 -0
- package/dist/runtime/testing/playwright-helpers.js +127 -0
- package/dist/runtime/types/error.d.ts +48 -0
- package/dist/runtime/types/error.js +0 -0
- package/dist/runtime/types/index.d.ts +2 -1
- package/dist/runtime/types/module.d.ts +15 -0
- package/dist/types.d.mts +3 -1
- package/package.json +15 -2
package/dist/module.d.mts
CHANGED
|
@@ -2,7 +2,8 @@ import * as _nuxt_schema from '@nuxt/schema';
|
|
|
2
2
|
import { LtExtensionsModuleOptions } from '../dist/runtime/types/index.js';
|
|
3
3
|
export { LtAuthClientConfig, LtAuthMode, LtAuthResponse, LtAuthState, LtPasskeyAuthResult, LtPasskeyRegisterResult, LtUser, UseLtAuthReturn } from '../dist/runtime/types/auth.js';
|
|
4
4
|
export { LtFileInfo, LtUploadItem, LtUploadOptions, LtUploadProgress, LtUploadStatus, UseLtFileReturn, UseLtTusUploadReturn } from '../dist/runtime/types/upload.js';
|
|
5
|
-
export { LtAuthModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtI18nModuleOptions, LtTusModuleOptions } from '../dist/runtime/types/module.js';
|
|
5
|
+
export { LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtI18nModuleOptions, LtTusModuleOptions } from '../dist/runtime/types/module.js';
|
|
6
|
+
export { LtErrorTranslationResponse, LtParsedError, UseLtErrorTranslationReturn } from '../dist/runtime/types/error.js';
|
|
6
7
|
|
|
7
8
|
declare const name = "@lenne.tech/nuxt-extensions";
|
|
8
9
|
declare const version = "1.0.0";
|
package/dist/module.mjs
CHANGED
|
@@ -18,6 +18,10 @@ const defaultOptions = {
|
|
|
18
18
|
loginPath: "/auth/login",
|
|
19
19
|
twoFactorRedirectPath: "/auth/2fa"
|
|
20
20
|
},
|
|
21
|
+
errorTranslation: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
defaultLocale: "de"
|
|
24
|
+
},
|
|
21
25
|
i18n: {
|
|
22
26
|
autoMerge: true
|
|
23
27
|
},
|
|
@@ -38,6 +42,7 @@ const module$1 = defineNuxtModule({
|
|
|
38
42
|
const { resolve } = createResolver(import.meta.url);
|
|
39
43
|
const resolvedOptions = {
|
|
40
44
|
auth: { ...defaultOptions.auth, ...options.auth },
|
|
45
|
+
errorTranslation: { ...defaultOptions.errorTranslation, ...options.errorTranslation },
|
|
41
46
|
i18n: { ...defaultOptions.i18n, ...options.i18n },
|
|
42
47
|
tus: { ...defaultOptions.tus, ...options.tus }
|
|
43
48
|
};
|
|
@@ -56,6 +61,10 @@ const module$1 = defineNuxtModule({
|
|
|
56
61
|
loginPath: resolvedOptions.auth?.loginPath || "/auth/login",
|
|
57
62
|
twoFactorRedirectPath: resolvedOptions.auth?.twoFactorRedirectPath || "/auth/2fa"
|
|
58
63
|
},
|
|
64
|
+
errorTranslation: {
|
|
65
|
+
enabled: resolvedOptions.errorTranslation?.enabled ?? true,
|
|
66
|
+
defaultLocale: resolvedOptions.errorTranslation?.defaultLocale || "de"
|
|
67
|
+
},
|
|
59
68
|
tus: {
|
|
60
69
|
defaultChunkSize: resolvedOptions.tus?.defaultChunkSize || 5 * 1024 * 1024,
|
|
61
70
|
defaultEndpoint: resolvedOptions.tus?.defaultEndpoint || "/files/upload"
|
|
@@ -66,6 +75,10 @@ const module$1 = defineNuxtModule({
|
|
|
66
75
|
{ name: "useLtAuth", from: resolve("./runtime/composables/auth/use-lt-auth") },
|
|
67
76
|
{ name: "useLtAuthClient", from: resolve("./runtime/composables/use-lt-auth-client") },
|
|
68
77
|
{ name: "ltAuthClient", from: resolve("./runtime/composables/use-lt-auth-client") },
|
|
78
|
+
{
|
|
79
|
+
name: "useLtErrorTranslation",
|
|
80
|
+
from: resolve("./runtime/composables/use-lt-error-translation")
|
|
81
|
+
},
|
|
69
82
|
{ name: "useLtFile", from: resolve("./runtime/composables/use-lt-file") },
|
|
70
83
|
{ name: "useLtTusUpload", from: resolve("./runtime/composables/use-lt-tus-upload") },
|
|
71
84
|
{ name: "useLtShare", from: resolve("./runtime/composables/use-lt-share") },
|
|
@@ -102,6 +115,9 @@ const module$1 = defineNuxtModule({
|
|
|
102
115
|
if (resolvedOptions.auth?.enabled && resolvedOptions.auth?.interceptor?.enabled) {
|
|
103
116
|
addPlugin(resolve("./runtime/plugins/auth-interceptor.client"));
|
|
104
117
|
}
|
|
118
|
+
if (resolvedOptions.errorTranslation?.enabled) {
|
|
119
|
+
addPlugin(resolve("./runtime/plugins/error-translation.client"));
|
|
120
|
+
}
|
|
105
121
|
if (resolvedOptions.i18n?.autoMerge) {
|
|
106
122
|
nuxt.hook(
|
|
107
123
|
"i18n:registerModule",
|
|
@@ -3,3 +3,4 @@ export { useLtAuthClient, ltAuthClient } from "./use-lt-auth-client.js";
|
|
|
3
3
|
export { useLtTusUpload } from "./use-lt-tus-upload.js";
|
|
4
4
|
export { useLtFile } from "./use-lt-file.js";
|
|
5
5
|
export { useLtShare, type UseLtShareReturn } from "./use-lt-share.js";
|
|
6
|
+
export { useLtErrorTranslation } from "./use-lt-error-translation.js";
|
|
@@ -3,3 +3,4 @@ export { useLtAuthClient, ltAuthClient } from "./use-lt-auth-client.js";
|
|
|
3
3
|
export { useLtTusUpload } from "./use-lt-tus-upload.js";
|
|
4
4
|
export { useLtFile } from "./use-lt-file.js";
|
|
5
5
|
export { useLtShare } from "./use-lt-share.js";
|
|
6
|
+
export { useLtErrorTranslation } from "./use-lt-error-translation.js";
|
|
@@ -17,11 +17,19 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
import { type LtAuthClient } from "../lib/auth-client.js";
|
|
20
|
+
/**
|
|
21
|
+
* Reset the auth client singleton (useful for testing or config changes)
|
|
22
|
+
*/
|
|
23
|
+
export declare function resetLtAuthClient(): void;
|
|
20
24
|
/**
|
|
21
25
|
* Returns the Better-Auth client singleton
|
|
22
26
|
*
|
|
23
27
|
* The client is created once and reused across all calls.
|
|
24
28
|
* Configuration is read from RuntimeConfig on first call.
|
|
29
|
+
*
|
|
30
|
+
* IMPORTANT: In dev mode, the basePath is automatically prefixed with '/api'
|
|
31
|
+
* to leverage Nuxt's server proxy. This is required for WebAuthn/Passkey
|
|
32
|
+
* to work correctly (same-origin policy).
|
|
25
33
|
*/
|
|
26
34
|
export declare function useLtAuthClient(): LtAuthClient;
|
|
27
35
|
export declare const ltAuthClient: {
|
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
import { useNuxtApp } from "#imports";
|
|
2
2
|
import { createLtAuthClient } from "../lib/auth-client.js";
|
|
3
3
|
let authClientInstance = null;
|
|
4
|
+
export function resetLtAuthClient() {
|
|
5
|
+
authClientInstance = null;
|
|
6
|
+
}
|
|
7
|
+
function isDevMode() {
|
|
8
|
+
if (import.meta.server) {
|
|
9
|
+
return process.env.NODE_ENV !== "production";
|
|
10
|
+
}
|
|
11
|
+
if (typeof window !== "undefined") {
|
|
12
|
+
const buildId = window.__NUXT__?.config?.app?.buildId;
|
|
13
|
+
if (buildId === "dev") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const hostname = window.location?.hostname;
|
|
17
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
4
23
|
export function useLtAuthClient() {
|
|
5
24
|
if (!authClientInstance) {
|
|
6
25
|
try {
|
|
7
26
|
const nuxtApp = useNuxtApp();
|
|
8
27
|
const config = nuxtApp.$config?.public?.ltExtensions?.auth || {};
|
|
28
|
+
const isDev = isDevMode();
|
|
29
|
+
let basePath = config.basePath || "/iam";
|
|
30
|
+
if (isDev && basePath && !basePath.startsWith("/api")) {
|
|
31
|
+
basePath = `/api${basePath}`;
|
|
32
|
+
}
|
|
9
33
|
authClientInstance = createLtAuthClient({
|
|
10
|
-
baseURL: config.baseURL,
|
|
11
|
-
basePath
|
|
34
|
+
baseURL: isDev ? "" : config.baseURL,
|
|
35
|
+
basePath,
|
|
12
36
|
twoFactorRedirectPath: config.twoFactorRedirectPath,
|
|
13
37
|
enableAdmin: config.enableAdmin,
|
|
14
38
|
enableTwoFactor: config.enableTwoFactor,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Translation Composable
|
|
3
|
+
*
|
|
4
|
+
* Translates backend error codes to user-friendly messages.
|
|
5
|
+
* Works with or without @nuxtjs/i18n.
|
|
6
|
+
*
|
|
7
|
+
* Backend error format: "#LTNS_0100: Unauthorized - User is not logged in"
|
|
8
|
+
* Translations loaded from: GET /api/i18n/errors/:locale
|
|
9
|
+
*/
|
|
10
|
+
import type { UseLtErrorTranslationReturn } from "../types/error.js";
|
|
11
|
+
/**
|
|
12
|
+
* Error Translation composable
|
|
13
|
+
*
|
|
14
|
+
* @returns Functions and state for error translation
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const { translateError, showErrorToast, parseError } = useLtErrorTranslation();
|
|
19
|
+
*
|
|
20
|
+
* // Translate error from API response
|
|
21
|
+
* const message = translateError(error.message);
|
|
22
|
+
* // Input: "#LTNS_0100: Unauthorized - User is not logged in"
|
|
23
|
+
* // Output: "Sie sind nicht angemeldet." (when locale is 'de')
|
|
24
|
+
*
|
|
25
|
+
* // Show error as toast
|
|
26
|
+
* showErrorToast(error, 'Login fehlgeschlagen');
|
|
27
|
+
*
|
|
28
|
+
* // Parse error for custom handling
|
|
29
|
+
* const parsed = parseError(error.message);
|
|
30
|
+
* // { code: 'LTNS_0100', developerMessage: 'Unauthorized...', translatedMessage: '...' }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function useLtErrorTranslation(): UseLtErrorTranslationReturn;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { computed, ref, useState, useNuxtApp, useRuntimeConfig } from "#imports";
|
|
2
|
+
const ERROR_CODE_REGEX = /^#([A-Z_]+_\d+):\s*(.+)$/;
|
|
3
|
+
export function useLtErrorTranslation() {
|
|
4
|
+
const nuxtApp = useNuxtApp();
|
|
5
|
+
const runtimeConfig = useRuntimeConfig();
|
|
6
|
+
const translations = useState(
|
|
7
|
+
"lt-error-translations",
|
|
8
|
+
() => ({})
|
|
9
|
+
);
|
|
10
|
+
const isLoading = ref(false);
|
|
11
|
+
const config = runtimeConfig.public?.ltExtensions?.errorTranslation;
|
|
12
|
+
function t(key, germanFallback) {
|
|
13
|
+
const i18n = nuxtApp.$i18n;
|
|
14
|
+
if (!i18n?.t) {
|
|
15
|
+
return germanFallback;
|
|
16
|
+
}
|
|
17
|
+
return i18n.t(key);
|
|
18
|
+
}
|
|
19
|
+
function detectLocale() {
|
|
20
|
+
const i18n = nuxtApp.$i18n;
|
|
21
|
+
if (i18n?.locale?.value) {
|
|
22
|
+
return i18n.locale.value;
|
|
23
|
+
}
|
|
24
|
+
if (import.meta.client && navigator?.language) {
|
|
25
|
+
const browserLang = navigator.language.split("-")[0] ?? "de";
|
|
26
|
+
if (["de", "en"].includes(browserLang)) {
|
|
27
|
+
return browserLang;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return config?.defaultLocale || "de";
|
|
31
|
+
}
|
|
32
|
+
function getApiBase() {
|
|
33
|
+
return runtimeConfig.public?.ltExtensions?.auth?.baseURL || "";
|
|
34
|
+
}
|
|
35
|
+
async function loadTranslations(locale) {
|
|
36
|
+
const targetLocale = locale || detectLocale();
|
|
37
|
+
if (translations.value[targetLocale]) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (isLoading.value) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
isLoading.value = true;
|
|
44
|
+
try {
|
|
45
|
+
const apiBase = getApiBase();
|
|
46
|
+
const response = await $fetch(
|
|
47
|
+
`${apiBase}/api/i18n/errors/${targetLocale}`
|
|
48
|
+
);
|
|
49
|
+
if (response?.errors) {
|
|
50
|
+
translations.value = {
|
|
51
|
+
...translations.value,
|
|
52
|
+
[targetLocale]: response.errors
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.warn(`[LtErrorTranslation] Failed to load translations for ${targetLocale}:`, error);
|
|
57
|
+
} finally {
|
|
58
|
+
isLoading.value = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function extractMessage(errorOrMessage) {
|
|
62
|
+
if (typeof errorOrMessage === "string") {
|
|
63
|
+
return errorOrMessage;
|
|
64
|
+
}
|
|
65
|
+
if (errorOrMessage instanceof Error) {
|
|
66
|
+
return errorOrMessage.message;
|
|
67
|
+
}
|
|
68
|
+
if (typeof errorOrMessage === "object" && errorOrMessage !== null) {
|
|
69
|
+
const obj = errorOrMessage;
|
|
70
|
+
return obj.message || obj.error?.message || obj.data?.message || obj.statusMessage || String(obj);
|
|
71
|
+
}
|
|
72
|
+
return String(errorOrMessage);
|
|
73
|
+
}
|
|
74
|
+
function parseError(errorOrMessage) {
|
|
75
|
+
const message = extractMessage(errorOrMessage);
|
|
76
|
+
const match = message.match(ERROR_CODE_REGEX);
|
|
77
|
+
if (match) {
|
|
78
|
+
const code = match[1] || "";
|
|
79
|
+
const developerMessage = match[2] || "";
|
|
80
|
+
const locale = detectLocale();
|
|
81
|
+
const localeTranslations = translations.value[locale] || {};
|
|
82
|
+
const translatedMessage = localeTranslations[code] || developerMessage;
|
|
83
|
+
return {
|
|
84
|
+
code,
|
|
85
|
+
developerMessage,
|
|
86
|
+
translatedMessage
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
code: null,
|
|
91
|
+
developerMessage: message,
|
|
92
|
+
translatedMessage: message
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function translateError(errorOrMessage) {
|
|
96
|
+
const parsed = parseError(errorOrMessage);
|
|
97
|
+
return parsed.translatedMessage;
|
|
98
|
+
}
|
|
99
|
+
function showErrorToast(errorOrMessage, title) {
|
|
100
|
+
if (!import.meta.client) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const parsed = parseError(errorOrMessage);
|
|
104
|
+
try {
|
|
105
|
+
nuxtApp.runWithContext(() => {
|
|
106
|
+
const toastComposable = nuxtApp.useToast || globalThis.useToast;
|
|
107
|
+
if (typeof toastComposable === "function") {
|
|
108
|
+
const toast = toastComposable();
|
|
109
|
+
toast.add({
|
|
110
|
+
color: "error",
|
|
111
|
+
title: title || t("lt.error.title", "Fehler"),
|
|
112
|
+
description: parsed.translatedMessage
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
console.error("[LtErrorTranslation]", parsed.translatedMessage);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
} catch {
|
|
119
|
+
console.error("[LtErrorTranslation]", parsed.translatedMessage);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const currentLocale = computed(() => detectLocale());
|
|
123
|
+
const isLoaded = computed(() => !!translations.value[currentLocale.value]);
|
|
124
|
+
return {
|
|
125
|
+
translateError,
|
|
126
|
+
parseError,
|
|
127
|
+
showErrorToast,
|
|
128
|
+
loadTranslations,
|
|
129
|
+
isLoaded,
|
|
130
|
+
isLoading,
|
|
131
|
+
currentLocale
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -3,9 +3,9 @@ import { adminClient, twoFactorClient } from "better-auth/client/plugins";
|
|
|
3
3
|
import { createAuthClient } from "better-auth/vue";
|
|
4
4
|
import { navigateTo } from "#imports";
|
|
5
5
|
import { ltSha256 } from "../utils/crypto.js";
|
|
6
|
-
import { createLtAuthFetch } from "./auth-state.js";
|
|
6
|
+
import { createLtAuthFetch, isLtDevMode } from "./auth-state.js";
|
|
7
7
|
export function createLtAuthClient(config = {}) {
|
|
8
|
-
const isDev =
|
|
8
|
+
const isDev = isLtDevMode();
|
|
9
9
|
const defaultBaseURL = isDev ? "" : import.meta.env?.VITE_API_URL || process.env.API_URL || "http://localhost:3000";
|
|
10
10
|
const defaultBasePath = isDev ? "/api/iam" : "/iam";
|
|
11
11
|
const {
|
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
* The state is persisted in cookies for SSR compatibility.
|
|
13
13
|
*/
|
|
14
14
|
import type { LtAuthMode } from "../types/index.js";
|
|
15
|
+
/**
|
|
16
|
+
* Detects if we're running in development mode at runtime.
|
|
17
|
+
*
|
|
18
|
+
* Note: `import.meta.dev` is evaluated at build time and doesn't work
|
|
19
|
+
* correctly for pre-built modules. This function uses runtime checks instead.
|
|
20
|
+
*
|
|
21
|
+
* @returns true if running in development mode
|
|
22
|
+
*/
|
|
23
|
+
export declare function isLtDevMode(): boolean;
|
|
15
24
|
/**
|
|
16
25
|
* Get the current auth mode from cookie
|
|
17
26
|
*/
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
export function isLtDevMode() {
|
|
2
|
+
if (import.meta.server) {
|
|
3
|
+
return process.env.NODE_ENV !== "production";
|
|
4
|
+
}
|
|
5
|
+
if (typeof window !== "undefined") {
|
|
6
|
+
const buildId = window.__NUXT__?.config?.app?.buildId;
|
|
7
|
+
if (buildId === "dev") {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
const hostname = window.location?.hostname;
|
|
11
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
1
17
|
export function getLtAuthMode() {
|
|
2
18
|
if (import.meta.server) return "cookie";
|
|
3
19
|
try {
|
|
@@ -53,7 +69,7 @@ export function setLtAuthMode(mode) {
|
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
71
|
export function getLtApiBase(basePath = "/iam") {
|
|
56
|
-
const isDev =
|
|
72
|
+
const isDev = isLtDevMode();
|
|
57
73
|
if (isDev) {
|
|
58
74
|
return `/api${basePath}`;
|
|
59
75
|
}
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"registerOptionsError": "Konnte Registrierungsoptionen nicht laden",
|
|
12
12
|
"sessionExpired": "Sitzung abgelaufen"
|
|
13
13
|
},
|
|
14
|
+
"error": {
|
|
15
|
+
"title": "Fehler",
|
|
16
|
+
"loadFailed": "Fehlerübersetzungen konnten nicht geladen werden"
|
|
17
|
+
},
|
|
14
18
|
"share": {
|
|
15
19
|
"copied": "Link kopiert",
|
|
16
20
|
"copiedDescription": "Der Link wurde in die Zwischenablage kopiert."
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"registerOptionsError": "Could not load registration options",
|
|
12
12
|
"sessionExpired": "Session expired"
|
|
13
13
|
},
|
|
14
|
+
"error": {
|
|
15
|
+
"title": "Error",
|
|
16
|
+
"loadFailed": "Failed to load error translations"
|
|
17
|
+
},
|
|
14
18
|
"share": {
|
|
15
19
|
"copied": "Link copied",
|
|
16
20
|
"copiedDescription": "The link has been copied to clipboard."
|
|
@@ -15,8 +15,9 @@ export default (nuxtApp) => {
|
|
|
15
15
|
];
|
|
16
16
|
const publicAuthPaths = [.../* @__PURE__ */ new Set([...defaultPublicPaths, ...configuredPublicPaths])];
|
|
17
17
|
function isPublicAuthRoute() {
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const router = nuxtApp.$router;
|
|
19
|
+
const route = router?.currentRoute?.value;
|
|
20
|
+
if (!route?.path) return false;
|
|
20
21
|
return publicAuthPaths.some((path) => route.path.startsWith(path));
|
|
21
22
|
}
|
|
22
23
|
function isAuthEndpoint(url) {
|
|
@@ -56,7 +57,8 @@ export default (nuxtApp) => {
|
|
|
56
57
|
if (isAuthenticated.value) {
|
|
57
58
|
console.debug("[LtAuth Interceptor] Session expired, logging out...");
|
|
58
59
|
clearUser();
|
|
59
|
-
const
|
|
60
|
+
const router = nuxtApp.$router;
|
|
61
|
+
const currentPath = router?.currentRoute?.value?.fullPath;
|
|
60
62
|
const redirectQuery = currentPath && currentPath !== loginPath ? `?redirect=${encodeURIComponent(currentPath)}` : "";
|
|
61
63
|
window.location.href = loginPath + redirectQuery;
|
|
62
64
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Translation Plugin
|
|
3
|
+
*
|
|
4
|
+
* Automatically loads error translations on app start.
|
|
5
|
+
* Provides global helper methods: $ltTranslateError, $ltShowErrorToast
|
|
6
|
+
*/
|
|
7
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig } from "#imports";
|
|
2
|
+
import { useLtErrorTranslation } from "../composables/use-lt-error-translation.js";
|
|
3
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
4
|
+
const runtimeConfig = useRuntimeConfig();
|
|
5
|
+
const config = runtimeConfig.public?.ltExtensions?.errorTranslation;
|
|
6
|
+
if (config?.enabled === false) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const { loadTranslations, translateError, showErrorToast } = useLtErrorTranslation();
|
|
10
|
+
try {
|
|
11
|
+
await loadTranslations();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.warn("[LtErrorTranslation] Initial load failed:", error);
|
|
14
|
+
}
|
|
15
|
+
nuxtApp.provide("ltTranslateError", translateError);
|
|
16
|
+
nuxtApp.provide("ltShowErrorToast", showErrorToast);
|
|
17
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing Utilities for @lenne.tech/nuxt-extensions
|
|
3
|
+
*
|
|
4
|
+
* This module exports Playwright test helpers for E2E testing
|
|
5
|
+
* of Nuxt applications using @lenne.tech/nuxt-extensions.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // In your test file
|
|
10
|
+
* import {
|
|
11
|
+
* waitForHydration,
|
|
12
|
+
* gotoAndWaitForHydration,
|
|
13
|
+
* fillInput,
|
|
14
|
+
* generateTestUser,
|
|
15
|
+
* generateTOTP,
|
|
16
|
+
* } from '@lenne.tech/nuxt-extensions/testing';
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
export { waitForHydration, gotoAndWaitForHydration, waitForURLAndHydration, DEFAULT_HYDRATION_TIMEOUT, fillInput, fillInputs, generateTestUser, generateRandomString, generateTOTP, extractTOTPSecret, parseTOTPUrl, waitForElement, waitForNetworkIdle, hasText, getAllText, type TestUser, } from "./playwright-helpers.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
waitForHydration,
|
|
3
|
+
gotoAndWaitForHydration,
|
|
4
|
+
waitForURLAndHydration,
|
|
5
|
+
DEFAULT_HYDRATION_TIMEOUT,
|
|
6
|
+
fillInput,
|
|
7
|
+
fillInputs,
|
|
8
|
+
generateTestUser,
|
|
9
|
+
generateRandomString,
|
|
10
|
+
generateTOTP,
|
|
11
|
+
extractTOTPSecret,
|
|
12
|
+
parseTOTPUrl,
|
|
13
|
+
waitForElement,
|
|
14
|
+
waitForNetworkIdle,
|
|
15
|
+
hasText,
|
|
16
|
+
getAllText
|
|
17
|
+
} from "./playwright-helpers.js";
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright Test Helpers for Nuxt Applications
|
|
3
|
+
*
|
|
4
|
+
* This module provides reusable test utilities for E2E testing
|
|
5
|
+
* with Playwright in Nuxt projects using @lenne.tech/nuxt-extensions.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import {
|
|
10
|
+
* waitForHydration,
|
|
11
|
+
* gotoAndWaitForHydration,
|
|
12
|
+
* fillInput,
|
|
13
|
+
* generateTestUser,
|
|
14
|
+
* generateTOTP,
|
|
15
|
+
* } from '@lenne.tech/nuxt-extensions/testing';
|
|
16
|
+
*
|
|
17
|
+
* test('my test', async ({ page }) => {
|
|
18
|
+
* await gotoAndWaitForHydration(page, '/auth/login');
|
|
19
|
+
* await fillInput(page, 'input[name="email"]', 'test@example.com');
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import type { Page } from "@playwright/test";
|
|
24
|
+
/**
|
|
25
|
+
* Default timeout for hydration waiting (in milliseconds)
|
|
26
|
+
*/
|
|
27
|
+
export declare const DEFAULT_HYDRATION_TIMEOUT = 15000;
|
|
28
|
+
/**
|
|
29
|
+
* Wait for Nuxt hydration to complete
|
|
30
|
+
*
|
|
31
|
+
* This is essential for Nuxt SSR applications where Vue components
|
|
32
|
+
* need to be hydrated before they can respond to interactions.
|
|
33
|
+
*
|
|
34
|
+
* @param page - Playwright page object
|
|
35
|
+
* @param options - Configuration options
|
|
36
|
+
* @param options.timeout - Maximum time to wait (default: 15000ms)
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* await waitForHydration(page);
|
|
41
|
+
* await page.click('button'); // Now safe to interact
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function waitForHydration(page: Page, options?: {
|
|
45
|
+
timeout?: number;
|
|
46
|
+
}): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Navigate to URL and wait for Nuxt hydration to complete
|
|
49
|
+
*
|
|
50
|
+
* Combines page.goto() with hydration waiting. Use this instead of
|
|
51
|
+
* page.goto() when you need to interact with Vue components immediately.
|
|
52
|
+
*
|
|
53
|
+
* @param page - Playwright page object
|
|
54
|
+
* @param url - URL to navigate to (relative or absolute)
|
|
55
|
+
* @param options - Configuration options
|
|
56
|
+
* @param options.timeout - Maximum time to wait for hydration (default: 15000ms)
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* await gotoAndWaitForHydration(page, '/auth/login');
|
|
61
|
+
* // Page is now hydrated and ready for interaction
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function gotoAndWaitForHydration(page: Page, url: string, options?: {
|
|
65
|
+
timeout?: number;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Wait for URL navigation and then Nuxt hydration
|
|
69
|
+
*
|
|
70
|
+
* Use after form submissions or actions that trigger navigation.
|
|
71
|
+
* Ensures both the navigation completes AND the new page is hydrated.
|
|
72
|
+
*
|
|
73
|
+
* @param page - Playwright page object
|
|
74
|
+
* @param url - URL pattern to wait for (string or RegExp)
|
|
75
|
+
* @param options - Configuration options
|
|
76
|
+
* @param options.timeout - Maximum time to wait for URL (default: 30000ms)
|
|
77
|
+
* @param options.hydrationTimeout - Maximum time to wait for hydration (default: 15000ms)
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* await page.click('button[type="submit"]');
|
|
82
|
+
* await waitForURLAndHydration(page, /\/dashboard/);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare function waitForURLAndHydration(page: Page, url: string | RegExp, options?: {
|
|
86
|
+
timeout?: number;
|
|
87
|
+
hydrationTimeout?: number;
|
|
88
|
+
}): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Fill an input field with proper Vue 3 reactivity support
|
|
91
|
+
*
|
|
92
|
+
* Uses keyboard typing instead of Playwright's fill() to properly
|
|
93
|
+
* trigger Vue's v-model updates. This is necessary because fill()
|
|
94
|
+
* sets the value directly without firing input events in some cases.
|
|
95
|
+
*
|
|
96
|
+
* @param page - Playwright page object
|
|
97
|
+
* @param selector - CSS selector for the input element
|
|
98
|
+
* @param value - Value to type into the input
|
|
99
|
+
* @param options - Configuration options
|
|
100
|
+
* @param options.delay - Delay between keystrokes in ms (default: 5)
|
|
101
|
+
* @param options.clear - Whether to clear existing content (default: true)
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* await fillInput(page, 'input[name="email"]', 'test@example.com');
|
|
106
|
+
* await fillInput(page, '#password', 'secret', { delay: 10 });
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function fillInput(page: Page, selector: string, value: string, options?: {
|
|
110
|
+
delay?: number;
|
|
111
|
+
clear?: boolean;
|
|
112
|
+
}): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Fill multiple input fields at once
|
|
115
|
+
*
|
|
116
|
+
* Convenience function for filling multiple form fields.
|
|
117
|
+
*
|
|
118
|
+
* @param page - Playwright page object
|
|
119
|
+
* @param fields - Object mapping selectors to values
|
|
120
|
+
* @param options - Configuration options passed to fillInput
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* await fillInputs(page, {
|
|
125
|
+
* 'input[name="email"]': 'test@example.com',
|
|
126
|
+
* 'input[name="password"]': 'secret123',
|
|
127
|
+
* });
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export declare function fillInputs(page: Page, fields: Record<string, string>, options?: {
|
|
131
|
+
delay?: number;
|
|
132
|
+
clear?: boolean;
|
|
133
|
+
}): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Test user credentials interface
|
|
136
|
+
*/
|
|
137
|
+
export interface TestUser {
|
|
138
|
+
email: string;
|
|
139
|
+
password: string;
|
|
140
|
+
name: string;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Generate unique test user credentials
|
|
144
|
+
*
|
|
145
|
+
* Creates unique email, password, and name for test isolation.
|
|
146
|
+
* Each call generates different credentials to avoid test conflicts.
|
|
147
|
+
*
|
|
148
|
+
* @param prefix - Prefix for the email address (default: 'e2e')
|
|
149
|
+
* @returns Object with email, password, and name
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const user = generateTestUser('auth-test');
|
|
154
|
+
* // { email: 'auth-test-abc123@test.com', password: 'TestPassabc123!', name: 'E2E Test User auth-test' }
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export declare function generateTestUser(prefix?: string): TestUser;
|
|
158
|
+
/**
|
|
159
|
+
* Generate a random string for test data
|
|
160
|
+
*
|
|
161
|
+
* @param length - Length of the string (default: 8)
|
|
162
|
+
* @param charset - Characters to use (default: alphanumeric)
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* const id = generateRandomString(12);
|
|
167
|
+
* const code = generateRandomString(6, '0123456789');
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export declare function generateRandomString(length?: number, charset?: string): string;
|
|
171
|
+
/**
|
|
172
|
+
* Generate a TOTP code from a secret (RFC 6238)
|
|
173
|
+
*
|
|
174
|
+
* Implements the Time-based One-Time Password algorithm for testing
|
|
175
|
+
* 2FA functionality. Compatible with Google Authenticator and similar apps.
|
|
176
|
+
*
|
|
177
|
+
* @param secret - Base32-encoded TOTP secret
|
|
178
|
+
* @param options - Configuration options
|
|
179
|
+
* @param options.digits - Number of digits in the code (default: 6)
|
|
180
|
+
* @param options.period - Time period in seconds (default: 30)
|
|
181
|
+
* @param options.algorithm - Hash algorithm (default: 'sha1')
|
|
182
|
+
* @returns 6-digit TOTP code as string
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* const secret = 'JBSWY3DPEHPK3PXP';
|
|
187
|
+
* const code = generateTOTP(secret);
|
|
188
|
+
* await page.fill('input[name="code"]', code);
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export declare function generateTOTP(secret: string, options?: {
|
|
192
|
+
digits?: number;
|
|
193
|
+
period?: number;
|
|
194
|
+
algorithm?: "sha1" | "sha256" | "sha512";
|
|
195
|
+
}): string;
|
|
196
|
+
/**
|
|
197
|
+
* Extract TOTP secret from otpauth:// URL
|
|
198
|
+
*
|
|
199
|
+
* Parses the secret parameter from a TOTP QR code URL.
|
|
200
|
+
*
|
|
201
|
+
* @param otpauthUrl - The otpauth:// URL (or data URL containing it)
|
|
202
|
+
* @returns The extracted secret, or null if not found
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const qrSrc = await page.locator('img[alt="QR Code"]').getAttribute('src');
|
|
207
|
+
* const secret = extractTOTPSecret(qrSrc);
|
|
208
|
+
* const code = generateTOTP(secret);
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export declare function extractTOTPSecret(otpauthUrl: string): string | null;
|
|
212
|
+
/**
|
|
213
|
+
* Extract all parameters from otpauth:// URL
|
|
214
|
+
*
|
|
215
|
+
* @param otpauthUrl - The otpauth:// URL
|
|
216
|
+
* @returns Object with issuer, account, secret, algorithm, digits, period
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* const params = parseTOTPUrl(otpauthUrl);
|
|
221
|
+
* console.log(params.issuer, params.secret);
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export declare function parseTOTPUrl(otpauthUrl: string): {
|
|
225
|
+
issuer: string | null;
|
|
226
|
+
account: string | null;
|
|
227
|
+
secret: string | null;
|
|
228
|
+
algorithm: string;
|
|
229
|
+
digits: number;
|
|
230
|
+
period: number;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Wait for an element to be visible and stable
|
|
234
|
+
*
|
|
235
|
+
* Useful for waiting for animations to complete or elements to settle.
|
|
236
|
+
*
|
|
237
|
+
* @param page - Playwright page object
|
|
238
|
+
* @param selector - CSS selector for the element
|
|
239
|
+
* @param options - Configuration options
|
|
240
|
+
* @param options.timeout - Maximum time to wait (default: 10000ms)
|
|
241
|
+
* @param options.stable - Wait for element to be stable (default: true)
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* await waitForElement(page, '.modal-content');
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
export declare function waitForElement(page: Page, selector: string, options?: {
|
|
249
|
+
timeout?: number;
|
|
250
|
+
stable?: boolean;
|
|
251
|
+
}): Promise<void>;
|
|
252
|
+
/**
|
|
253
|
+
* Wait for network to be idle
|
|
254
|
+
*
|
|
255
|
+
* Useful for waiting for all API calls to complete.
|
|
256
|
+
*
|
|
257
|
+
* @param page - Playwright page object
|
|
258
|
+
* @param options - Configuration options
|
|
259
|
+
* @param options.timeout - Maximum time to wait (default: 30000ms)
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* await page.click('button[type="submit"]');
|
|
264
|
+
* await waitForNetworkIdle(page);
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
export declare function waitForNetworkIdle(page: Page, options?: {
|
|
268
|
+
timeout?: number;
|
|
269
|
+
}): Promise<void>;
|
|
270
|
+
/**
|
|
271
|
+
* Check if an element contains specific text
|
|
272
|
+
*
|
|
273
|
+
* @param page - Playwright page object
|
|
274
|
+
* @param selector - CSS selector for the element
|
|
275
|
+
* @param text - Text to search for
|
|
276
|
+
* @returns true if text is found
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```typescript
|
|
280
|
+
* const hasError = await hasText(page, '.error', 'Invalid email');
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
export declare function hasText(page: Page, selector: string, text: string): Promise<boolean>;
|
|
284
|
+
/**
|
|
285
|
+
* Get all text content from matching elements
|
|
286
|
+
*
|
|
287
|
+
* @param page - Playwright page object
|
|
288
|
+
* @param selector - CSS selector for the elements
|
|
289
|
+
* @returns Array of text content
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* const errors = await getAllText(page, '.error-message');
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
export declare function getAllText(page: Page, selector: string): Promise<string[]>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
export const DEFAULT_HYDRATION_TIMEOUT = 15e3;
|
|
3
|
+
export async function waitForHydration(page, options = {}) {
|
|
4
|
+
const timeout = options.timeout ?? DEFAULT_HYDRATION_TIMEOUT;
|
|
5
|
+
await page.waitForFunction(() => window.useNuxtApp?.()?.isHydrating === false, {
|
|
6
|
+
timeout
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export async function gotoAndWaitForHydration(page, url, options = {}) {
|
|
10
|
+
await page.goto(url);
|
|
11
|
+
await waitForHydration(page, options);
|
|
12
|
+
}
|
|
13
|
+
export async function waitForURLAndHydration(page, url, options = {}) {
|
|
14
|
+
const { timeout = 3e4, hydrationTimeout } = options;
|
|
15
|
+
await page.waitForURL(url, { timeout });
|
|
16
|
+
await waitForHydration(page, { timeout: hydrationTimeout });
|
|
17
|
+
}
|
|
18
|
+
export async function fillInput(page, selector, value, options = {}) {
|
|
19
|
+
const { delay = 5, clear = true } = options;
|
|
20
|
+
const locator = page.locator(selector);
|
|
21
|
+
await locator.click();
|
|
22
|
+
if (clear) {
|
|
23
|
+
await page.keyboard.press("Control+a");
|
|
24
|
+
await page.keyboard.press("Backspace");
|
|
25
|
+
}
|
|
26
|
+
await page.keyboard.type(value, { delay });
|
|
27
|
+
}
|
|
28
|
+
export async function fillInputs(page, fields, options = {}) {
|
|
29
|
+
for (const [selector, value] of Object.entries(fields)) {
|
|
30
|
+
await fillInput(page, selector, value, options);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function generateTestUser(prefix = "e2e") {
|
|
34
|
+
const testId = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
35
|
+
return {
|
|
36
|
+
email: `${prefix}-${testId}@test.com`,
|
|
37
|
+
password: `TestPass${testId}!`,
|
|
38
|
+
name: `E2E Test User ${prefix}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function generateRandomString(length = 8, charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") {
|
|
42
|
+
let result = "";
|
|
43
|
+
for (let i = 0; i < length; i++) {
|
|
44
|
+
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
export function generateTOTP(secret, options = {}) {
|
|
49
|
+
const { digits = 6, period = 30, algorithm = "sha1" } = options;
|
|
50
|
+
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
51
|
+
let bits = "";
|
|
52
|
+
for (const char of secret.toUpperCase().replace(/=/g, "")) {
|
|
53
|
+
const val = base32Chars.indexOf(char);
|
|
54
|
+
if (val === -1) continue;
|
|
55
|
+
bits += val.toString(2).padStart(5, "0");
|
|
56
|
+
}
|
|
57
|
+
const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
|
|
58
|
+
for (let i = 0; i < keyBytes.length; i++) {
|
|
59
|
+
keyBytes[i] = parseInt(bits.slice(i * 8, (i + 1) * 8), 2);
|
|
60
|
+
}
|
|
61
|
+
const counter = Math.floor(Date.now() / 1e3 / period);
|
|
62
|
+
const counterBuffer = Buffer.alloc(8);
|
|
63
|
+
counterBuffer.writeBigUInt64BE(BigInt(counter));
|
|
64
|
+
const hmac = crypto.createHmac(algorithm, keyBytes);
|
|
65
|
+
hmac.update(counterBuffer);
|
|
66
|
+
const hash = hmac.digest();
|
|
67
|
+
const offset = (hash[hash.length - 1] ?? 0) & 15;
|
|
68
|
+
const code = ((hash[offset] ?? 0) & 127) << 24 | ((hash[offset + 1] ?? 0) & 255) << 16 | ((hash[offset + 2] ?? 0) & 255) << 8 | (hash[offset + 3] ?? 0) & 255;
|
|
69
|
+
const mod = Math.pow(10, digits);
|
|
70
|
+
return (code % mod).toString().padStart(digits, "0");
|
|
71
|
+
}
|
|
72
|
+
export function extractTOTPSecret(otpauthUrl) {
|
|
73
|
+
const decoded = decodeURIComponent(otpauthUrl);
|
|
74
|
+
const match = decoded.match(/secret=([A-Z2-7]+)/i);
|
|
75
|
+
return match?.[1]?.toUpperCase() ?? null;
|
|
76
|
+
}
|
|
77
|
+
export function parseTOTPUrl(otpauthUrl) {
|
|
78
|
+
const decoded = decodeURIComponent(otpauthUrl);
|
|
79
|
+
const pathMatch = decoded.match(/otpauth:\/\/totp\/(?:([^:]+):)?([^?]+)/);
|
|
80
|
+
const getParam = (name) => {
|
|
81
|
+
const match = decoded.match(new RegExp(`${name}=([^&]+)`, "i"));
|
|
82
|
+
return match?.[1] ?? null;
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
issuer: getParam("issuer") ?? pathMatch?.[1] ?? null,
|
|
86
|
+
account: pathMatch?.[2] ?? null,
|
|
87
|
+
secret: getParam("secret")?.toUpperCase() ?? null,
|
|
88
|
+
algorithm: getParam("algorithm") || "SHA1",
|
|
89
|
+
digits: parseInt(getParam("digits") || "6", 10),
|
|
90
|
+
period: parseInt(getParam("period") || "30", 10)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export async function waitForElement(page, selector, options = {}) {
|
|
94
|
+
const { timeout = 1e4, stable = true } = options;
|
|
95
|
+
const locator = page.locator(selector);
|
|
96
|
+
await locator.waitFor({ state: "visible", timeout });
|
|
97
|
+
if (stable) {
|
|
98
|
+
await locator.evaluate((el) => {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
if (!el.getAnimations || el.getAnimations().length === 0) {
|
|
101
|
+
resolve();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
Promise.all(el.getAnimations().map((a) => a.finished)).then(() => resolve());
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function waitForNetworkIdle(page, options = {}) {
|
|
110
|
+
const { timeout = 3e4 } = options;
|
|
111
|
+
await page.waitForLoadState("networkidle", { timeout });
|
|
112
|
+
}
|
|
113
|
+
export async function hasText(page, selector, text) {
|
|
114
|
+
const locator = page.locator(selector);
|
|
115
|
+
const content = await locator.textContent();
|
|
116
|
+
return content?.includes(text) ?? false;
|
|
117
|
+
}
|
|
118
|
+
export async function getAllText(page, selector) {
|
|
119
|
+
const locators = page.locator(selector);
|
|
120
|
+
const count = await locators.count();
|
|
121
|
+
const texts = [];
|
|
122
|
+
for (let i = 0; i < count; i++) {
|
|
123
|
+
const text = await locators.nth(i).textContent();
|
|
124
|
+
if (text) texts.push(text.trim());
|
|
125
|
+
}
|
|
126
|
+
return texts;
|
|
127
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ComputedRef, Ref } from "vue";
|
|
2
|
+
/**
|
|
3
|
+
* Backend error translation response format
|
|
4
|
+
* Matches the response from GET /api/i18n/errors/:locale
|
|
5
|
+
*/
|
|
6
|
+
export interface LtErrorTranslationResponse {
|
|
7
|
+
errors: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parsed error from backend
|
|
11
|
+
* Contains both the original and translated messages
|
|
12
|
+
*/
|
|
13
|
+
export interface LtParsedError {
|
|
14
|
+
/** Raw error code (e.g., LTNS_0100) or null if no code found */
|
|
15
|
+
code: string | null;
|
|
16
|
+
/** Original developer message from the backend */
|
|
17
|
+
developerMessage: string;
|
|
18
|
+
/** Translated user-friendly message */
|
|
19
|
+
translatedMessage: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Error translation module options
|
|
23
|
+
*/
|
|
24
|
+
export interface LtErrorTranslationModuleOptions {
|
|
25
|
+
/** Enable error translation feature (default: true) */
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
/** Default locale if not detected (default: 'de') */
|
|
28
|
+
defaultLocale?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Return type for useLtErrorTranslation composable
|
|
32
|
+
*/
|
|
33
|
+
export interface UseLtErrorTranslationReturn {
|
|
34
|
+
/** Translate an error message or code to user-friendly message */
|
|
35
|
+
translateError: (errorOrMessage: unknown) => string;
|
|
36
|
+
/** Parse a backend error to extract code and messages */
|
|
37
|
+
parseError: (errorOrMessage: unknown) => LtParsedError;
|
|
38
|
+
/** Show translated error as toast notification */
|
|
39
|
+
showErrorToast: (errorOrMessage: unknown, title?: string) => void;
|
|
40
|
+
/** Manually load translations for a locale */
|
|
41
|
+
loadTranslations: (locale?: string) => Promise<void>;
|
|
42
|
+
/** Check if translations are loaded for current locale */
|
|
43
|
+
isLoaded: ComputedRef<boolean>;
|
|
44
|
+
/** Check if translations are currently loading */
|
|
45
|
+
isLoading: Ref<boolean>;
|
|
46
|
+
/** Current detected locale */
|
|
47
|
+
currentLocale: ComputedRef<string>;
|
|
48
|
+
}
|
|
File without changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export type { LtAuthClientConfig, LtAuthMode, LtAuthResponse, LtAuthState, LtPasskeyAuthResult, LtPasskeyRegisterResult, LtUser, UseLtAuthReturn, } from "./auth.js";
|
|
2
2
|
export type { LtFileInfo, LtUploadItem, LtUploadOptions, LtUploadProgress, LtUploadStatus, UseLtFileReturn, UseLtTusUploadReturn, } from "./upload.js";
|
|
3
|
-
export type { LtAuthModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtI18nModuleOptions, LtTusModuleOptions, } from "./module.js";
|
|
3
|
+
export type { LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtI18nModuleOptions, LtTusModuleOptions, } from "./module.js";
|
|
4
|
+
export type { LtErrorTranslationResponse, LtParsedError, UseLtErrorTranslationReturn, } from "./error.js";
|
|
@@ -42,12 +42,23 @@ export interface LtI18nModuleOptions {
|
|
|
42
42
|
/** Automatically merge locale files with @nuxtjs/i18n (default: true) */
|
|
43
43
|
autoMerge?: boolean;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Error translation module configuration options
|
|
47
|
+
*/
|
|
48
|
+
export interface LtErrorTranslationModuleOptions {
|
|
49
|
+
/** Enable error translation feature (default: true) */
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
/** Default locale if not detected (default: 'de') */
|
|
52
|
+
defaultLocale?: string;
|
|
53
|
+
}
|
|
45
54
|
/**
|
|
46
55
|
* Main module options for @lenne.tech/nuxt-extensions
|
|
47
56
|
*/
|
|
48
57
|
export interface LtExtensionsModuleOptions {
|
|
49
58
|
/** Auth module configuration */
|
|
50
59
|
auth?: LtAuthModuleOptions;
|
|
60
|
+
/** Error translation configuration */
|
|
61
|
+
errorTranslation?: LtErrorTranslationModuleOptions;
|
|
51
62
|
/** i18n configuration */
|
|
52
63
|
i18n?: LtI18nModuleOptions;
|
|
53
64
|
/** TUS upload module configuration */
|
|
@@ -72,6 +83,10 @@ export interface LtExtensionsPublicRuntimeConfig {
|
|
|
72
83
|
loginPath: string;
|
|
73
84
|
twoFactorRedirectPath: string;
|
|
74
85
|
};
|
|
86
|
+
errorTranslation: {
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
defaultLocale: string;
|
|
89
|
+
};
|
|
75
90
|
tus: {
|
|
76
91
|
defaultChunkSize: number;
|
|
77
92
|
defaultEndpoint: string;
|
package/dist/types.d.mts
CHANGED
|
@@ -8,6 +8,8 @@ export { type LtAuthClientConfig, type LtAuthMode, type LtAuthResponse, type LtA
|
|
|
8
8
|
|
|
9
9
|
export { type LtFileInfo, type LtUploadItem, type LtUploadOptions, type LtUploadProgress, type LtUploadStatus, type UseLtFileReturn, type UseLtTusUploadReturn } from '../dist/runtime/types/upload.js'
|
|
10
10
|
|
|
11
|
-
export { type LtAuthModuleOptions, type LtExtensionsModuleOptions, type LtExtensionsPublicRuntimeConfig, type LtI18nModuleOptions, type LtTusModuleOptions } from '../dist/runtime/types/module.js'
|
|
11
|
+
export { type LtAuthModuleOptions, type LtErrorTranslationModuleOptions, type LtExtensionsModuleOptions, type LtExtensionsPublicRuntimeConfig, type LtI18nModuleOptions, type LtTusModuleOptions } from '../dist/runtime/types/module.js'
|
|
12
|
+
|
|
13
|
+
export { type LtErrorTranslationResponse, type LtParsedError, type UseLtErrorTranslationReturn } from '../dist/runtime/types/error.js'
|
|
12
14
|
|
|
13
15
|
export { type configKey, default, type name, type version } from './module.mjs'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nuxt-extensions",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/types.d.mts",
|
|
19
19
|
"import": "./dist/module.mjs"
|
|
20
|
+
},
|
|
21
|
+
"./testing": {
|
|
22
|
+
"types": "./dist/runtime/testing/index.d.ts",
|
|
23
|
+
"import": "./dist/runtime/testing/index.js"
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"main": "./dist/module.mjs",
|
|
@@ -24,6 +28,9 @@
|
|
|
24
28
|
"*": {
|
|
25
29
|
".": [
|
|
26
30
|
"./dist/types.d.mts"
|
|
31
|
+
],
|
|
32
|
+
"testing": [
|
|
33
|
+
"./dist/runtime/testing/index.d.ts"
|
|
27
34
|
]
|
|
28
35
|
}
|
|
29
36
|
},
|
|
@@ -33,6 +40,7 @@
|
|
|
33
40
|
"scripts": {
|
|
34
41
|
"prepare": "git config core.hooksPath .githooks",
|
|
35
42
|
"prepack": "nuxt-module-build build",
|
|
43
|
+
"check": "npm run format && npm run lint:fix && npm run test:types && npm run test",
|
|
36
44
|
"dev": "npm run dev:prepare && nuxt dev playground",
|
|
37
45
|
"dev:build": "nuxt build playground",
|
|
38
46
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
@@ -45,13 +53,14 @@
|
|
|
45
53
|
"test:watch": "vitest watch",
|
|
46
54
|
"test:coverage": "vitest run --coverage",
|
|
47
55
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
|
|
48
|
-
"release": "npm run format:check && npm run lint && npm run test && npm run prepack"
|
|
56
|
+
"release": "npm run format:check && npm run lint && npm run test:types && npm run test && npm run prepack"
|
|
49
57
|
},
|
|
50
58
|
"dependencies": {
|
|
51
59
|
"@nuxt/kit": "4.2.2"
|
|
52
60
|
},
|
|
53
61
|
"peerDependencies": {
|
|
54
62
|
"@better-auth/passkey": ">=1.0.0",
|
|
63
|
+
"@playwright/test": ">=1.0.0",
|
|
55
64
|
"better-auth": ">=1.0.0",
|
|
56
65
|
"nuxt": ">=3.0.0",
|
|
57
66
|
"tus-js-client": ">=4.0.0"
|
|
@@ -63,6 +72,9 @@
|
|
|
63
72
|
"@better-auth/passkey": {
|
|
64
73
|
"optional": true
|
|
65
74
|
},
|
|
75
|
+
"@playwright/test": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
66
78
|
"tus-js-client": {
|
|
67
79
|
"optional": true
|
|
68
80
|
}
|
|
@@ -70,6 +82,7 @@
|
|
|
70
82
|
"devDependencies": {
|
|
71
83
|
"@better-auth/passkey": "1.4.10",
|
|
72
84
|
"@nuxt/devtools": "3.1.1",
|
|
85
|
+
"@playwright/test": "1.57.0",
|
|
73
86
|
"@nuxt/module-builder": "1.0.2",
|
|
74
87
|
"@nuxt/schema": "4.2.2",
|
|
75
88
|
"@nuxt/test-utils": "3.23.0",
|