@scayle/storefront-nuxt 7.36.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/CHANGELOG.md +598 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +2 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +8 -0
- package/dist/module.d.ts +8 -0
- package/dist/module.json +8 -0
- package/dist/module.mjs +173 -0
- package/dist/rpc.d.mts +12 -0
- package/dist/rpc.d.ts +12 -0
- package/dist/rpc.mjs +18 -0
- package/dist/runtime/api/cacheAuth.d.ts +12 -0
- package/dist/runtime/api/cacheAuth.mjs +40 -0
- package/dist/runtime/api/purgeAll.d.ts +2 -0
- package/dist/runtime/api/purgeAll.mjs +14 -0
- package/dist/runtime/api/purgeTags.d.ts +2 -0
- package/dist/runtime/api/purgeTags.mjs +19 -0
- package/dist/runtime/api/rpcHandler.d.ts +2 -0
- package/dist/runtime/api/rpcHandler.mjs +15 -0
- package/dist/runtime/api/up.d.ts +4 -0
- package/dist/runtime/api/up.mjs +4 -0
- package/dist/runtime/composables/core/useAvailableShops.d.ts +2 -0
- package/dist/runtime/composables/core/useAvailableShops.mjs +4 -0
- package/dist/runtime/composables/core/useCurrentShop.d.ts +3 -0
- package/dist/runtime/composables/core/useCurrentShop.mjs +8 -0
- package/dist/runtime/composables/core/useEventListener.d.ts +2 -0
- package/dist/runtime/composables/core/useEventListener.mjs +2 -0
- package/dist/runtime/composables/core/useLog.d.ts +1 -0
- package/dist/runtime/composables/core/useLog.mjs +8 -0
- package/dist/runtime/composables/core/useRpc.d.ts +17 -0
- package/dist/runtime/composables/core/useRpc.mjs +56 -0
- package/dist/runtime/composables/core/useSession.d.ts +21 -0
- package/dist/runtime/composables/core/useSession.mjs +21 -0
- package/dist/runtime/composables/core/useUser.d.ts +16 -0
- package/dist/runtime/composables/core/useUser.mjs +68 -0
- package/dist/runtime/composables/storefront/useBasket.d.ts +54 -0
- package/dist/runtime/composables/storefront/useBasket.mjs +165 -0
- package/dist/runtime/composables/storefront/useBrand.d.ts +16 -0
- package/dist/runtime/composables/storefront/useBrand.mjs +20 -0
- package/dist/runtime/composables/storefront/useBrands.d.ts +16 -0
- package/dist/runtime/composables/storefront/useBrands.mjs +20 -0
- package/dist/runtime/composables/storefront/useCategories.d.ts +20 -0
- package/dist/runtime/composables/storefront/useCategories.mjs +30 -0
- package/dist/runtime/composables/storefront/useCurrentPromotions.d.ts +16 -0
- package/dist/runtime/composables/storefront/useCurrentPromotions.mjs +20 -0
- package/dist/runtime/composables/storefront/useFacet.d.ts +48 -0
- package/dist/runtime/composables/storefront/useFacet.mjs +193 -0
- package/dist/runtime/composables/storefront/useFilters.d.ts +16 -0
- package/dist/runtime/composables/storefront/useFilters.mjs +20 -0
- package/dist/runtime/composables/storefront/useNavigationTree.d.ts +16 -0
- package/dist/runtime/composables/storefront/useNavigationTree.mjs +20 -0
- package/dist/runtime/composables/storefront/useNavigationTrees.d.ts +16 -0
- package/dist/runtime/composables/storefront/useNavigationTrees.mjs +20 -0
- package/dist/runtime/composables/storefront/useOrder.d.ts +89 -0
- package/dist/runtime/composables/storefront/useOrder.mjs +17 -0
- package/dist/runtime/composables/storefront/useOrderConfirmation.d.ts +89 -0
- package/dist/runtime/composables/storefront/useOrderConfirmation.mjs +17 -0
- package/dist/runtime/composables/storefront/useProduct.d.ts +16 -0
- package/dist/runtime/composables/storefront/useProduct.mjs +20 -0
- package/dist/runtime/composables/storefront/useProducts.d.ts +20 -0
- package/dist/runtime/composables/storefront/useProducts.mjs +20 -0
- package/dist/runtime/composables/storefront/useProductsByIds.d.ts +16 -0
- package/dist/runtime/composables/storefront/useProductsByIds.mjs +20 -0
- package/dist/runtime/composables/storefront/useProductsCount.d.ts +16 -0
- package/dist/runtime/composables/storefront/useProductsCount.mjs +20 -0
- package/dist/runtime/composables/storefront/usePromotions.d.ts +16 -0
- package/dist/runtime/composables/storefront/usePromotions.mjs +20 -0
- package/dist/runtime/composables/storefront/usePromotionsByIds.d.ts +16 -0
- package/dist/runtime/composables/storefront/usePromotionsByIds.mjs +20 -0
- package/dist/runtime/composables/storefront/useQueryFilterState.d.ts +12 -0
- package/dist/runtime/composables/storefront/useQueryFilterState.mjs +66 -0
- package/dist/runtime/composables/storefront/useSearch.d.ts +13 -0
- package/dist/runtime/composables/storefront/useSearch.mjs +49 -0
- package/dist/runtime/composables/storefront/useShopConfiguration.d.ts +13 -0
- package/dist/runtime/composables/storefront/useShopConfiguration.mjs +19 -0
- package/dist/runtime/composables/storefront/useUserAddresses.d.ts +13 -0
- package/dist/runtime/composables/storefront/useUserAddresses.mjs +19 -0
- package/dist/runtime/composables/storefront/useVariant.d.ts +16 -0
- package/dist/runtime/composables/storefront/useVariant.mjs +20 -0
- package/dist/runtime/composables/storefront/useWishlist.d.ts +48 -0
- package/dist/runtime/composables/storefront/useWishlist.mjs +119 -0
- package/dist/runtime/composables/useCoreLog.d.ts +1 -0
- package/dist/runtime/composables/useCoreLog.mjs +4 -0
- package/dist/runtime/context.d.ts +12 -0
- package/dist/runtime/context.mjs +98 -0
- package/dist/runtime/createLog.d.ts +3 -0
- package/dist/runtime/createLog.mjs +39 -0
- package/dist/runtime/error/handler.d.ts +14 -0
- package/dist/runtime/error/handler.mjs +53 -0
- package/dist/runtime/handler.d.ts +8 -0
- package/dist/runtime/handler.mjs +26 -0
- package/dist/runtime/log.d.ts +38 -0
- package/dist/runtime/log.mjs +68 -0
- package/dist/runtime/plugin/log.client.d.ts +8 -0
- package/dist/runtime/plugin/log.client.mjs +24 -0
- package/dist/runtime/plugin/log.server.d.ts +2 -0
- package/dist/runtime/plugin/log.server.mjs +11 -0
- package/dist/runtime/plugin/shop.d.ts +8 -0
- package/dist/runtime/plugin/shop.mjs +17 -0
- package/dist/runtime/rpc/rpcCall.d.ts +9 -0
- package/dist/runtime/rpc/rpcCall.mjs +16 -0
- package/dist/runtime/server/middleware/bootstrap.d.ts +12 -0
- package/dist/runtime/server/middleware/bootstrap.mjs +188 -0
- package/dist/runtime/server/middleware/bootstrap.test.d.ts +6 -0
- package/dist/runtime/server/middleware/bootstrap.test.mjs +277 -0
- package/dist/runtime/server/middleware/bootstrap.utils.d.ts +7 -0
- package/dist/runtime/server/middleware/bootstrap.utils.mjs +59 -0
- package/dist/runtime/server/middleware/cache.d.ts +12 -0
- package/dist/runtime/server/middleware/cache.mjs +44 -0
- package/dist/runtime/server/middleware/oauth.d.ts +3 -0
- package/dist/runtime/server/middleware/oauth.mjs +123 -0
- package/dist/runtime/server/middleware/redirects.d.ts +2 -0
- package/dist/runtime/server/middleware/redirects.mjs +86 -0
- package/dist/runtime/server/middleware/redirects.test.d.ts +1 -0
- package/dist/runtime/server/middleware/redirects.test.mjs +66 -0
- package/dist/runtime/server/middleware/redirects.utils.d.ts +18 -0
- package/dist/runtime/server/middleware/redirects.utils.mjs +28 -0
- package/dist/runtime/utils/error.d.ts +5 -0
- package/dist/runtime/utils/error.mjs +7 -0
- package/dist/runtime/utils/price.d.ts +11 -0
- package/dist/runtime/utils/price.mjs +32 -0
- package/dist/runtime/utils/price.test.d.ts +1 -0
- package/dist/runtime/utils/price.test.mjs +83 -0
- package/dist/runtime/utils/route.d.ts +2 -0
- package/dist/runtime/utils/route.mjs +3 -0
- package/dist/runtime/utils/route.test.d.ts +1 -0
- package/dist/runtime/utils/route.test.mjs +15 -0
- package/dist/shared/storefront-nuxt.9548700f.d.mts +161 -0
- package/dist/shared/storefront-nuxt.9548700f.d.ts +161 -0
- package/dist/types.d.mts +15 -0
- package/dist/types.d.ts +15 -0
- package/package.json +103 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { errors as joseErrors, jwtVerify } from "jose";
|
|
2
|
+
import { rpcMethods } from "@scayle/storefront-core";
|
|
3
|
+
async function updateSession(event, data) {
|
|
4
|
+
const session = event.context.session;
|
|
5
|
+
Object.assign(session.data, data);
|
|
6
|
+
await session.save();
|
|
7
|
+
}
|
|
8
|
+
const updateUser = async (event, tokens) => {
|
|
9
|
+
const checkoutUrl = event.context.$rpcContext.checkout.url;
|
|
10
|
+
const shopId = event.context.$currentShop.shopId;
|
|
11
|
+
const user = await rpcMethods.fetchUser(
|
|
12
|
+
{ accessToken: tokens.access_token, callback: "" },
|
|
13
|
+
{
|
|
14
|
+
checkoutUrl,
|
|
15
|
+
shopId
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
await updateSession(event, {
|
|
19
|
+
accessToken: tokens.access_token,
|
|
20
|
+
refreshToken: tokens.refresh_token,
|
|
21
|
+
user
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const updateAccessTokenWithRefreshToken = async (event, sessionData, log) => {
|
|
25
|
+
if (!sessionData?.refreshToken) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const context = event.context.$rpcContext;
|
|
30
|
+
const updatedTokens = await rpcMethods.refreshAccessToken(context);
|
|
31
|
+
if (updatedTokens === null) {
|
|
32
|
+
await revokeTokensAndDestroySession(event, log);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await updateUser(event, updatedTokens);
|
|
37
|
+
return true;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
log.error("api/oauth/me", {
|
|
40
|
+
message: "fetching user failed",
|
|
41
|
+
error: e
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
log.error("v1/oauth/token", {
|
|
46
|
+
message: "refresh access token failed, so the local tokens were deleted",
|
|
47
|
+
error: e
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
const revokeTokensAndDestroySession = async (event, log) => {
|
|
53
|
+
log.debug("revokeTokensAndDestroySession");
|
|
54
|
+
const context = event.context.$rpcContext;
|
|
55
|
+
try {
|
|
56
|
+
await rpcMethods.oauthRevokeToken(context);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
log.error("v1/oauth/tokens", {
|
|
59
|
+
message: "error when revoking an access token",
|
|
60
|
+
error: e
|
|
61
|
+
});
|
|
62
|
+
} finally {
|
|
63
|
+
await context.destroySession();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const updateSessionWithUser = async (event, sessionData, log) => {
|
|
67
|
+
try {
|
|
68
|
+
const checkoutUrl = event.context.$rpcContext.checkout.url;
|
|
69
|
+
const shopId = event.context.$currentShop.shopId;
|
|
70
|
+
const user = await rpcMethods.fetchUser(
|
|
71
|
+
{ accessToken: sessionData.accessToken, callback: "" },
|
|
72
|
+
{
|
|
73
|
+
checkoutUrl,
|
|
74
|
+
shopId
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
await updateSession(event, {
|
|
78
|
+
user
|
|
79
|
+
});
|
|
80
|
+
} catch (e) {
|
|
81
|
+
log.error("api/oauth/me", {
|
|
82
|
+
message: "fetching user failed",
|
|
83
|
+
error: e
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
export async function useOauth(event, jwks) {
|
|
88
|
+
const context = event.context.$rpcContext;
|
|
89
|
+
const log = event.context.$rpcContext.log.space("core").space("session");
|
|
90
|
+
const sessionData = event.context.session.data;
|
|
91
|
+
log.debug("validating oauth");
|
|
92
|
+
if (!sessionData?.accessToken && !sessionData?.refreshToken) {
|
|
93
|
+
log.debug("no tokens saved on session; skipping validation");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!sessionData?.accessToken) {
|
|
97
|
+
log.debug("destroying session");
|
|
98
|
+
await context.destroySession();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
log.debug("validating existing session");
|
|
103
|
+
await jwtVerify(sessionData.accessToken, jwks, { clockTolerance: 5 });
|
|
104
|
+
if (!sessionData?.user) {
|
|
105
|
+
log.debug("updating user session");
|
|
106
|
+
await updateSessionWithUser(event, sessionData, log);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
} catch (e) {
|
|
110
|
+
log.debug("error validating session");
|
|
111
|
+
if (e instanceof joseErrors.JWTExpired) {
|
|
112
|
+
log.debug("token expired");
|
|
113
|
+
const wasTokenSuccessfullyRefreshed = await updateAccessTokenWithRefreshToken(event, sessionData, log);
|
|
114
|
+
if (wasTokenSuccessfullyRefreshed) {
|
|
115
|
+
log.debug("token successfully refreshed");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
log.error("Could not verify JWT", e);
|
|
120
|
+
}
|
|
121
|
+
log.debug("revoking token and destroying session");
|
|
122
|
+
await revokeTokensAndDestroySession(event, log);
|
|
123
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { sendRedirect, getRequestURL } from "h3";
|
|
2
|
+
import { getRedirectLookupUrls, getTargetLocation } from "./redirects.utils.mjs";
|
|
3
|
+
import { createCache } from "./cache.mjs";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
const REDIS_REDIRECT_PREFIX = ":REDIRECT";
|
|
6
|
+
const REDIRECT_NOT_SET = "REDIRECT_NOT_SET";
|
|
7
|
+
const REDIRECT_CACHE_TTL = 120;
|
|
8
|
+
async function fetchRedirectWithCache(sourceUrl, bapiClient, redirectCache, log) {
|
|
9
|
+
const cachedResult = await redirectCache.get(sourceUrl);
|
|
10
|
+
if (cachedResult) {
|
|
11
|
+
if (cachedResult === REDIRECT_NOT_SET) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return cachedResult;
|
|
15
|
+
}
|
|
16
|
+
log.debug("No cached result for redirect; querying BAPI");
|
|
17
|
+
let bapiResult;
|
|
18
|
+
try {
|
|
19
|
+
bapiResult = await bapiClient.redirects.post(sourceUrl);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
log.error(`Error: Failed to fetch redirects ... ${error}`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const isValidRedirect = (redirect) => {
|
|
25
|
+
return redirect && redirect?.source && redirect?.target && redirect.source !== redirect.target;
|
|
26
|
+
};
|
|
27
|
+
if (!bapiResult || !isValidRedirect(bapiResult)) {
|
|
28
|
+
await redirectCache.set(sourceUrl, REDIRECT_NOT_SET, REDIRECT_CACHE_TTL);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
await redirectCache.set(
|
|
32
|
+
bapiResult.source,
|
|
33
|
+
{
|
|
34
|
+
target: bapiResult.target,
|
|
35
|
+
statusCode: bapiResult.statusCode
|
|
36
|
+
},
|
|
37
|
+
REDIRECT_CACHE_TTL
|
|
38
|
+
);
|
|
39
|
+
return bapiResult;
|
|
40
|
+
}
|
|
41
|
+
export async function useRedirects(event) {
|
|
42
|
+
const config = useRuntimeConfig();
|
|
43
|
+
const $storefrontConfig = config.storefront;
|
|
44
|
+
const options = $storefrontConfig.redirects;
|
|
45
|
+
const url = getRequestURL(event, {
|
|
46
|
+
xForwardedProto: true,
|
|
47
|
+
xForwardedHost: true
|
|
48
|
+
});
|
|
49
|
+
const currentShop = event.context.$currentShop;
|
|
50
|
+
const log = event.context.$rpcContext.log.space("sfc").space("redirects");
|
|
51
|
+
const redirectCache = createCache(
|
|
52
|
+
"redis",
|
|
53
|
+
currentShop.shopId + REDIS_REDIRECT_PREFIX,
|
|
54
|
+
log,
|
|
55
|
+
{ redis: $storefrontConfig.redis }
|
|
56
|
+
);
|
|
57
|
+
const bapiClient = event.context.$rpcContext.bapiClient;
|
|
58
|
+
const queryParamWhitelist = new Set(options?.queryParamWhitelist ?? []);
|
|
59
|
+
log.debug(`Querying redirect for ${url.toString()}`);
|
|
60
|
+
const { relativeSourceUrl, absoluteSourceUrl } = getRedirectLookupUrls(
|
|
61
|
+
url,
|
|
62
|
+
queryParamWhitelist
|
|
63
|
+
);
|
|
64
|
+
const [relativeRedirect, absoluteRedirect] = await Promise.all([
|
|
65
|
+
fetchRedirectWithCache(relativeSourceUrl, bapiClient, redirectCache, log),
|
|
66
|
+
fetchRedirectWithCache(absoluteSourceUrl, bapiClient, redirectCache, log)
|
|
67
|
+
]);
|
|
68
|
+
if (relativeRedirect && absoluteRedirect) {
|
|
69
|
+
log.info(`There are multiple valid redirects for: ${url.toString()}`);
|
|
70
|
+
}
|
|
71
|
+
const redirect = relativeRedirect ?? absoluteRedirect;
|
|
72
|
+
try {
|
|
73
|
+
if (redirect?.target) {
|
|
74
|
+
log.debug(`Redirecting to ${redirect.target}`);
|
|
75
|
+
await sendRedirect(
|
|
76
|
+
event,
|
|
77
|
+
getTargetLocation(url, redirect.target, queryParamWhitelist),
|
|
78
|
+
redirect.statusCode
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
log.debug(`No redirect found`);
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
log.error(`Error: Failed to execute redirect ... ${error}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getRedirectLookupUrls, getTargetLocation } from "./redirects.utils.mjs";
|
|
3
|
+
describe("redirects module", () => {
|
|
4
|
+
describe("getRedirectLookupUrls", () => {
|
|
5
|
+
it("returns the relative and absolute URL", () => {
|
|
6
|
+
expect(
|
|
7
|
+
getRedirectLookupUrls(
|
|
8
|
+
new URL("https://demo.example/de/frauen/kleidung"),
|
|
9
|
+
/* @__PURE__ */ new Set()
|
|
10
|
+
)
|
|
11
|
+
).to.eql({
|
|
12
|
+
relativeSourceUrl: "/de/frauen/kleidung",
|
|
13
|
+
absoluteSourceUrl: "https://demo.example/de/frauen/kleidung"
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
it("strips trailing slashes", () => {
|
|
17
|
+
expect(
|
|
18
|
+
getRedirectLookupUrls(new URL("https://demo.example/men/"), /* @__PURE__ */ new Set())
|
|
19
|
+
).to.eql({
|
|
20
|
+
relativeSourceUrl: "/men",
|
|
21
|
+
absoluteSourceUrl: "https://demo.example/men"
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
it("strips query params", () => {
|
|
25
|
+
expect(
|
|
26
|
+
getRedirectLookupUrls(
|
|
27
|
+
new URL("https://demo.example/products?p=20&term=foo"),
|
|
28
|
+
/* @__PURE__ */ new Set()
|
|
29
|
+
)
|
|
30
|
+
).to.eql({
|
|
31
|
+
relativeSourceUrl: "/products",
|
|
32
|
+
absoluteSourceUrl: "https://demo.example/products"
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it("does not strip whitelisted query params", () => {
|
|
36
|
+
expect(
|
|
37
|
+
getRedirectLookupUrls(
|
|
38
|
+
new URL("https://demo.example/test/page/?foo=1&bar=10"),
|
|
39
|
+
/* @__PURE__ */ new Set(["bar"])
|
|
40
|
+
)
|
|
41
|
+
).to.eql({
|
|
42
|
+
relativeSourceUrl: "/test/page?bar=10",
|
|
43
|
+
absoluteSourceUrl: "https://demo.example/test/page?bar=10"
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe("getTargetLocation", () => {
|
|
48
|
+
it("uses the source as the base for relative URLs", () => {
|
|
49
|
+
expect(
|
|
50
|
+
getTargetLocation(
|
|
51
|
+
new URL("http://demo.test/source/location"),
|
|
52
|
+
"/target"
|
|
53
|
+
)
|
|
54
|
+
).to.eq("http://demo.test/target");
|
|
55
|
+
});
|
|
56
|
+
it("copies non-whitelisted query params from source", () => {
|
|
57
|
+
expect(
|
|
58
|
+
getTargetLocation(
|
|
59
|
+
new URL("http://demo.test/source?foo=10&bar=20"),
|
|
60
|
+
"/redirect",
|
|
61
|
+
/* @__PURE__ */ new Set(["foo"])
|
|
62
|
+
)
|
|
63
|
+
).to.eq("http://demo.test/redirect?bar=20");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get a normalized URL in absolute and relative forms for redirect lookup
|
|
3
|
+
* @param url
|
|
4
|
+
* @param queryParamWhitelist
|
|
5
|
+
*/
|
|
6
|
+
export declare function getRedirectLookupUrls(url: URL, queryParamWhitelist?: Set<string>): {
|
|
7
|
+
relativeSourceUrl: string;
|
|
8
|
+
absoluteSourceUrl: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Construct a URL string given a source URL and target redirect
|
|
12
|
+
* Query params are copied from source to target (if the params are not whitelisted)
|
|
13
|
+
* and source is used as the base URL if target is a relative redirect.
|
|
14
|
+
* @param source
|
|
15
|
+
* @param target
|
|
16
|
+
* @param queryParamWhitelist
|
|
17
|
+
*/
|
|
18
|
+
export declare function getTargetLocation(source: URL, target: string, queryParamWhitelist?: Set<string>): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function getRedirectLookupUrls(url, queryParamWhitelist) {
|
|
2
|
+
const incomingUrl = `${url.origin}${url.pathname}`;
|
|
3
|
+
const sourceUrlObject = new URL(
|
|
4
|
+
incomingUrl.endsWith("/") ? incomingUrl.slice(0, -1) : incomingUrl
|
|
5
|
+
);
|
|
6
|
+
if (queryParamWhitelist) {
|
|
7
|
+
for (const [key, value] of [...url.searchParams.entries()]) {
|
|
8
|
+
if (queryParamWhitelist.has(key)) {
|
|
9
|
+
sourceUrlObject.searchParams.append(key, value);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const relativeSourceUrl = decodeURI(
|
|
14
|
+
sourceUrlObject.toString().replace(sourceUrlObject.origin, "")
|
|
15
|
+
);
|
|
16
|
+
const absoluteSourceUrl = decodeURI(sourceUrlObject.toString());
|
|
17
|
+
return { relativeSourceUrl, absoluteSourceUrl };
|
|
18
|
+
}
|
|
19
|
+
export function getTargetLocation(source, target, queryParamWhitelist) {
|
|
20
|
+
const base = URL.canParse(target) ? target : source;
|
|
21
|
+
const targetUrl = new URL(target, base);
|
|
22
|
+
for (const [key, value] of source.searchParams.entries()) {
|
|
23
|
+
if (!queryParamWhitelist?.has(key) && !targetUrl.searchParams.has(key)) {
|
|
24
|
+
targetUrl.searchParams.append(key, value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return decodeURI(targetUrl.toString());
|
|
28
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { NuxtError } from 'nuxt/app';
|
|
2
|
+
import { FetchError } from 'ofetch';
|
|
3
|
+
import { parseErrorData } from '../error/handler';
|
|
4
|
+
declare const handleError: (error: FetchError | Error | unknown, additionalData?: unknown) => NuxtError;
|
|
5
|
+
export { handleError, parseErrorData };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createError } from "nuxt/app";
|
|
2
|
+
import { parseErrorData, resolveError } from "../error/handler.mjs";
|
|
3
|
+
const handleError = (error, additionalData) => {
|
|
4
|
+
const data = resolveError(error, additionalData);
|
|
5
|
+
return createError(data);
|
|
6
|
+
};
|
|
7
|
+
export { handleError, parseErrorData };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type BaseOptions = {
|
|
2
|
+
locale: string;
|
|
3
|
+
currencyFractionDigits?: number;
|
|
4
|
+
};
|
|
5
|
+
type FormatOptions = BaseOptions;
|
|
6
|
+
type ToCurrencyOptions = BaseOptions & {
|
|
7
|
+
currency: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const toCurrency: (value: number, options?: ToCurrencyOptions) => string;
|
|
10
|
+
export declare const formatPrice: (value: number, formatOptions?: FormatOptions) => string;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getDefaultFractionDigits } from "@scayle/storefront-core";
|
|
2
|
+
import { useCurrentShop } from "../composables/core/useCurrentShop.mjs";
|
|
3
|
+
export const toCurrency = (value, options) => {
|
|
4
|
+
const currentShop = useCurrentShop();
|
|
5
|
+
const {
|
|
6
|
+
locale = currentShop.value.locale,
|
|
7
|
+
currency = currentShop.value.currency,
|
|
8
|
+
currencyFractionDigits = currentShop.value.currencyFractionDigits
|
|
9
|
+
} = options || {};
|
|
10
|
+
const minimumFractionDigits = getDefaultFractionDigits(locale, currency);
|
|
11
|
+
return (value / 100).toLocaleString(locale, {
|
|
12
|
+
style: "currency",
|
|
13
|
+
...currencyFractionDigits && {
|
|
14
|
+
minimumFractionDigits: Math.min(
|
|
15
|
+
minimumFractionDigits,
|
|
16
|
+
currencyFractionDigits
|
|
17
|
+
)
|
|
18
|
+
},
|
|
19
|
+
maximumFractionDigits: currencyFractionDigits,
|
|
20
|
+
currency
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
export const formatPrice = (value, formatOptions) => {
|
|
24
|
+
const currentShop = useCurrentShop();
|
|
25
|
+
const {
|
|
26
|
+
locale = currentShop.value.locale,
|
|
27
|
+
currencyFractionDigits = currentShop.value.currencyFractionDigits
|
|
28
|
+
} = formatOptions || {};
|
|
29
|
+
return (value / 100).toLocaleString(locale, {
|
|
30
|
+
minimumFractionDigits: currencyFractionDigits
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { toCurrency, formatPrice } from "./price.mjs";
|
|
4
|
+
describe("Price utils", () => {
|
|
5
|
+
vi.mock("../composables/core/useCurrentShop", () => ({
|
|
6
|
+
useCurrentShop: () => ref({
|
|
7
|
+
locale: "de-DE",
|
|
8
|
+
currency: "EUR",
|
|
9
|
+
currencyFractionDigits: 2
|
|
10
|
+
})
|
|
11
|
+
}));
|
|
12
|
+
describe("toCurrency", () => {
|
|
13
|
+
it("should return value with EUR currency", () => {
|
|
14
|
+
const options = {
|
|
15
|
+
locale: "de-DE",
|
|
16
|
+
currency: "EUR",
|
|
17
|
+
currencyFractionDigits: 2
|
|
18
|
+
};
|
|
19
|
+
const currency = toCurrency(200, options);
|
|
20
|
+
expect(currency).be.eq("2,00\xA0\u20AC");
|
|
21
|
+
});
|
|
22
|
+
it("should return value with EUR currency with default options", () => {
|
|
23
|
+
const currency = toCurrency(200);
|
|
24
|
+
expect(currency).be.eq("2,00\xA0\u20AC");
|
|
25
|
+
});
|
|
26
|
+
it("should return value with EUR currency with default options", () => {
|
|
27
|
+
const currency = toCurrency(200);
|
|
28
|
+
expect(currency).be.eq("2,00\xA0\u20AC");
|
|
29
|
+
});
|
|
30
|
+
it("should return value with USD currency", () => {
|
|
31
|
+
const options = {
|
|
32
|
+
locale: "de-DE",
|
|
33
|
+
currency: "USD",
|
|
34
|
+
currencyFractionDigits: 2
|
|
35
|
+
};
|
|
36
|
+
const currency = toCurrency(200, options);
|
|
37
|
+
expect(currency).be.eq("2,00\xA0$");
|
|
38
|
+
});
|
|
39
|
+
it("should return value with one fraction digit", () => {
|
|
40
|
+
const options = {
|
|
41
|
+
locale: "de-DE",
|
|
42
|
+
currency: "USD",
|
|
43
|
+
currencyFractionDigits: 1
|
|
44
|
+
};
|
|
45
|
+
const currency = toCurrency(200, options);
|
|
46
|
+
expect(currency).be.eq("2,0\xA0$");
|
|
47
|
+
});
|
|
48
|
+
it("should return value with two fraction digits if no fraction digits set", () => {
|
|
49
|
+
const options = {
|
|
50
|
+
locale: "de-DE",
|
|
51
|
+
currency: "USD"
|
|
52
|
+
};
|
|
53
|
+
const currency = toCurrency(200, options);
|
|
54
|
+
expect(currency).be.eq("2,00\xA0$");
|
|
55
|
+
});
|
|
56
|
+
it("should return value properly formatted for en-US locale", () => {
|
|
57
|
+
const options = {
|
|
58
|
+
locale: "en-US",
|
|
59
|
+
currency: "USD"
|
|
60
|
+
};
|
|
61
|
+
const currency = toCurrency(200, options);
|
|
62
|
+
expect(currency).be.eq("$2.00");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("formatPrice", () => {
|
|
66
|
+
it("should return formatted price with two fraction digits", () => {
|
|
67
|
+
const options = {
|
|
68
|
+
locale: "en-US",
|
|
69
|
+
currencyFractionDigits: 2
|
|
70
|
+
};
|
|
71
|
+
const price = formatPrice(200, options);
|
|
72
|
+
expect(price).be.eq("2.00");
|
|
73
|
+
});
|
|
74
|
+
it("should return formatted price with one fraction digits", () => {
|
|
75
|
+
const options = {
|
|
76
|
+
locale: "en-US",
|
|
77
|
+
currencyFractionDigits: 1
|
|
78
|
+
};
|
|
79
|
+
const price = formatPrice(200, options);
|
|
80
|
+
expect(price).be.eq("2.0");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getCurrentPage } from "./route.mjs";
|
|
3
|
+
describe("getCurrentPage", () => {
|
|
4
|
+
it("should return parsed current page", () => {
|
|
5
|
+
const page = getCurrentPage({ page: "20" });
|
|
6
|
+
expect(page).be.equal(20);
|
|
7
|
+
});
|
|
8
|
+
it("should return undefined if page does not exist inside the query", () => {
|
|
9
|
+
const page = getCurrentPage({});
|
|
10
|
+
expect(page).be.equal(void 0);
|
|
11
|
+
});
|
|
12
|
+
it("should return undefined if page is not a string", () => {
|
|
13
|
+
expect(getCurrentPage({ page: ["1", "2"] })).toBe(void 0);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { HashAlgorithm, WithParams, ShopUser } from '@scayle/storefront-core';
|
|
2
|
+
|
|
3
|
+
type StorageProvider = 'memory' | 'redis';
|
|
4
|
+
type CheckoutShopConfig = {
|
|
5
|
+
host: string;
|
|
6
|
+
token: string;
|
|
7
|
+
secret: string;
|
|
8
|
+
user: string;
|
|
9
|
+
/**
|
|
10
|
+
* The number of seconds that a CBD token should be considered valid since issued
|
|
11
|
+
*/
|
|
12
|
+
cbdExpiration?: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Options to configure how the storefront core manages sessions
|
|
16
|
+
*/
|
|
17
|
+
type SessionConfig = Partial<{
|
|
18
|
+
/**
|
|
19
|
+
* The name used for the session cookie
|
|
20
|
+
*/
|
|
21
|
+
cookieName: string;
|
|
22
|
+
/**
|
|
23
|
+
* The sameSite policy to use for the session cookie
|
|
24
|
+
*/
|
|
25
|
+
sameSite: 'none' | 'lax' | 'strict';
|
|
26
|
+
/**
|
|
27
|
+
* The default maxAge (in seconds) set on the session cookie and default TTL for session store
|
|
28
|
+
*/
|
|
29
|
+
maxAge: number;
|
|
30
|
+
/**
|
|
31
|
+
* The secret used for signing session cookies. If an array is passed, the last
|
|
32
|
+
* value is used for signing new cookies, but all values are used to verify cookies.
|
|
33
|
+
*/
|
|
34
|
+
secret: string | string[];
|
|
35
|
+
/**
|
|
36
|
+
* Controls the domain option on the session cookie
|
|
37
|
+
*/
|
|
38
|
+
domain: string;
|
|
39
|
+
/**
|
|
40
|
+
* The provider to use for the persistent storage.
|
|
41
|
+
*/
|
|
42
|
+
provider: StorageProvider;
|
|
43
|
+
}>;
|
|
44
|
+
interface CacheConfig {
|
|
45
|
+
provider?: StorageProvider;
|
|
46
|
+
auth?: {
|
|
47
|
+
username: string;
|
|
48
|
+
password: string;
|
|
49
|
+
};
|
|
50
|
+
ssr?: {};
|
|
51
|
+
enabled?: boolean;
|
|
52
|
+
}
|
|
53
|
+
type RedisConfig = {
|
|
54
|
+
port?: number;
|
|
55
|
+
host?: string;
|
|
56
|
+
prefix?: string;
|
|
57
|
+
sslTransit?: boolean;
|
|
58
|
+
} | {
|
|
59
|
+
port?: number;
|
|
60
|
+
host?: string;
|
|
61
|
+
prefix?: string;
|
|
62
|
+
sslTransit?: boolean;
|
|
63
|
+
user: string;
|
|
64
|
+
password: string;
|
|
65
|
+
};
|
|
66
|
+
interface BapiConfig {
|
|
67
|
+
host: string;
|
|
68
|
+
token: string;
|
|
69
|
+
}
|
|
70
|
+
interface AppKeys {
|
|
71
|
+
wishlistKey: string;
|
|
72
|
+
basketKey: string;
|
|
73
|
+
hashAlgorithm: HashAlgorithm;
|
|
74
|
+
}
|
|
75
|
+
interface RedirectsConfig {
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
queryParamWhitelist?: string[];
|
|
78
|
+
}
|
|
79
|
+
interface OauthConfig {
|
|
80
|
+
apiHost: string;
|
|
81
|
+
clientId: string;
|
|
82
|
+
clientSecret: string;
|
|
83
|
+
}
|
|
84
|
+
type AuthenticationConfig = {
|
|
85
|
+
redirect: {
|
|
86
|
+
home: string;
|
|
87
|
+
logout: string;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
interface AdditionalShopConfig {
|
|
91
|
+
}
|
|
92
|
+
interface ShopConfig extends AdditionalShopConfig {
|
|
93
|
+
shopId: number;
|
|
94
|
+
domain: string;
|
|
95
|
+
path?: string;
|
|
96
|
+
locale: string;
|
|
97
|
+
apiBasePath?: string;
|
|
98
|
+
currency: string;
|
|
99
|
+
currencyFractionDigits?: number;
|
|
100
|
+
storeCampaignKeyword?: string;
|
|
101
|
+
isEnabled?: boolean;
|
|
102
|
+
checkout: CheckoutShopConfig;
|
|
103
|
+
sessionConfig?: SessionConfig;
|
|
104
|
+
auth: {
|
|
105
|
+
resetPasswordUrl: string;
|
|
106
|
+
};
|
|
107
|
+
redis?: RedisConfig;
|
|
108
|
+
bapi?: BapiConfig;
|
|
109
|
+
appKeys?: AppKeys;
|
|
110
|
+
}
|
|
111
|
+
type ShopConfigIndexed = Record<string, ShopConfig>;
|
|
112
|
+
interface PublicShopConfig extends Pick<ShopConfig, 'shopId' | 'domain' | 'path' | 'locale' | 'currency' | 'currencyFractionDigits'> {
|
|
113
|
+
checkout: Pick<CheckoutShopConfig, 'host'>;
|
|
114
|
+
apiBasePath: string;
|
|
115
|
+
}
|
|
116
|
+
interface StorefrontConfig {
|
|
117
|
+
session?: SessionConfig;
|
|
118
|
+
bapi: BapiConfig;
|
|
119
|
+
redis: RedisConfig;
|
|
120
|
+
auth: AuthenticationConfig;
|
|
121
|
+
oauth: OauthConfig;
|
|
122
|
+
appKeys: AppKeys;
|
|
123
|
+
apiBasePath?: string;
|
|
124
|
+
cache?: CacheConfig;
|
|
125
|
+
publicShopData?: (keyof AdditionalShopConfig)[];
|
|
126
|
+
withParams?: WithParams;
|
|
127
|
+
}
|
|
128
|
+
type ModuleOptions = StorefrontConfig & {
|
|
129
|
+
shopSelector: 'domain' | 'path';
|
|
130
|
+
stores: ShopConfigIndexed;
|
|
131
|
+
rpcDir?: string;
|
|
132
|
+
rpcMethodNames?: string[];
|
|
133
|
+
log: {
|
|
134
|
+
name: string;
|
|
135
|
+
};
|
|
136
|
+
redirects?: RedirectsConfig;
|
|
137
|
+
};
|
|
138
|
+
interface CheckoutEvent {
|
|
139
|
+
action?: 'authenticated';
|
|
140
|
+
type?: 'tracking';
|
|
141
|
+
user: ShopUser;
|
|
142
|
+
accessToken: string;
|
|
143
|
+
event?: {
|
|
144
|
+
event: 'login' | 'add_to_cart' | 'remove_from_cart';
|
|
145
|
+
status: 'successful' | 'error';
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
declare module '@nuxt/schema' {
|
|
149
|
+
interface RuntimeConfig {
|
|
150
|
+
storefront: ModuleOptions;
|
|
151
|
+
}
|
|
152
|
+
interface PublicRuntimeConfig {
|
|
153
|
+
imageBaseUrl: string;
|
|
154
|
+
log: {
|
|
155
|
+
name: string;
|
|
156
|
+
};
|
|
157
|
+
auth: AuthenticationConfig;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type { AppKeys as A, BapiConfig as B, CheckoutShopConfig as C, ModuleOptions as M, OauthConfig as O, PublicShopConfig as P, RedisConfig as R, StorageProvider as S, SessionConfig as a, CacheConfig as b, RedirectsConfig as c, AuthenticationConfig as d, AdditionalShopConfig as e, ShopConfig as f, ShopConfigIndexed as g, StorefrontConfig as h, CheckoutEvent as i };
|