@osimart/create-storefront 0.0.0-stage → 1.0.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/README.md +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.ts +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.ts +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +259 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +43 -3
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { watch } from "vue";
|
|
2
|
+
import { analytics } from "#osi/utils/analytics";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Starts Google Analytics 4 and the Meta Pixel, and reports page views.
|
|
6
|
+
*
|
|
7
|
+
* Client only (.client.js): analytics never runs during SSR, and the scripts
|
|
8
|
+
* load after hydration so they can't slow the first paint.
|
|
9
|
+
*
|
|
10
|
+
* The IDs come from the bootstrap response — `store.google_analytics_id` and
|
|
11
|
+
* `store.meta_pixel_id` — which app.vue has already put into the "storefront"
|
|
12
|
+
* state, so on the client it is there before this plugin runs. A store with
|
|
13
|
+
* neither ID gets nothing: no script, no request, every analytics.* call a
|
|
14
|
+
* no-op.
|
|
15
|
+
*
|
|
16
|
+
* Not loaded inside the dashboard's page-builder preview: a merchant editing
|
|
17
|
+
* their store must not show up as a visitor in their own reports.
|
|
18
|
+
*
|
|
19
|
+
* Page views are sent by hand, because the storefront is a single-page app.
|
|
20
|
+
* The first one is sent once the app has mounted; later ones when the next page
|
|
21
|
+
* has rendered, so `page_title` is the new page's, not the previous one's.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The builder iframe / preview routes — same test as app.vue's. */
|
|
25
|
+
function isBuilderPreview() {
|
|
26
|
+
try {
|
|
27
|
+
if (window.parent !== window || window.self !== window.top) return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return true; // cross-origin parent: we're framed
|
|
30
|
+
}
|
|
31
|
+
const { pathname, search } = window.location;
|
|
32
|
+
if (pathname.includes("__builder-preview")) return true;
|
|
33
|
+
const params = new URLSearchParams(search);
|
|
34
|
+
return params.has("osi_preview") || params.has("preview");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
38
|
+
if (isBuilderPreview()) return { provide: { analytics } };
|
|
39
|
+
|
|
40
|
+
const router = useRouter();
|
|
41
|
+
const storefront = useState("storefront");
|
|
42
|
+
|
|
43
|
+
let lastPath = null;
|
|
44
|
+
let fallbackTimer = null;
|
|
45
|
+
|
|
46
|
+
function trackPage() {
|
|
47
|
+
if (!analytics.isEnabled()) return;
|
|
48
|
+
const path = router.currentRoute.value.path;
|
|
49
|
+
// page:finish, app:mounted and the fallback below can all land on the same
|
|
50
|
+
// page — whichever comes first reports it, the rest see the same path.
|
|
51
|
+
if (path === lastPath) return;
|
|
52
|
+
lastPath = path;
|
|
53
|
+
analytics.pageView();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function start(store) {
|
|
57
|
+
const started = analytics.start({
|
|
58
|
+
gaIds: store?.google_analytics_id,
|
|
59
|
+
pixelIds: store?.meta_pixel_id,
|
|
60
|
+
currency:
|
|
61
|
+
store?.default_currency?.code ||
|
|
62
|
+
store?.currency_code ||
|
|
63
|
+
(typeof store?.currency === "string" ? store.currency : ""),
|
|
64
|
+
});
|
|
65
|
+
// Arrived after the app mounted: there is no app:mounted left to wait for.
|
|
66
|
+
if (started && nuxtApp.isHydrating === false) setTimeout(trackPage, 0);
|
|
67
|
+
return started;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Normally the store is already in state and this starts at once. Watching
|
|
71
|
+
// covers a bootstrap that finishes after the plugin ran.
|
|
72
|
+
let done = false;
|
|
73
|
+
watch(
|
|
74
|
+
() => storefront.value?.store,
|
|
75
|
+
(store) => {
|
|
76
|
+
if (done || !store) return;
|
|
77
|
+
done = true; // the bootstrap is one-shot: decide once
|
|
78
|
+
start(store);
|
|
79
|
+
},
|
|
80
|
+
{ immediate: true }
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// First page: after hydration, when document.title is final.
|
|
84
|
+
nuxtApp.hook("app:mounted", () => setTimeout(trackPage, 0));
|
|
85
|
+
|
|
86
|
+
// Every page after it: once it has rendered.
|
|
87
|
+
nuxtApp.hook("page:finish", () => setTimeout(trackPage, 0));
|
|
88
|
+
|
|
89
|
+
router.afterEach((to, _from, failure) => {
|
|
90
|
+
if (failure) return;
|
|
91
|
+
analytics.routeChanged(to.path);
|
|
92
|
+
// Safety net if page:finish doesn't fire for a navigation: harmless when
|
|
93
|
+
// it does, because trackPage() skips a path it has already reported.
|
|
94
|
+
clearTimeout(fallbackTimer);
|
|
95
|
+
fallbackTimer = setTimeout(trackPage, 2000);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return { provide: { analytics } };
|
|
99
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useUserStore } from '#osi/stores/userStore'
|
|
2
|
+
import { getBaseUrl } from '#osi/services/ApiServices'
|
|
3
|
+
|
|
4
|
+
export default defineNuxtPlugin(async () => {
|
|
5
|
+
const geoCookie = useCookie('geo', { maxAge: 60 * 60 * 24 * 30 }) // 30 days
|
|
6
|
+
|
|
7
|
+
// SSR or client: if cookie exists, hydrate the store immediately
|
|
8
|
+
if (geoCookie.value) {
|
|
9
|
+
useUserStore().setClientIpInfo(geoCookie.value)
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Client-only: fetch geo from backend (browser sends real user IP)
|
|
14
|
+
if (import.meta.client) {
|
|
15
|
+
try {
|
|
16
|
+
const baseUrl = getBaseUrl()
|
|
17
|
+
const data = await $fetch(`${baseUrl}client-ip/`)
|
|
18
|
+
geoCookie.value = data
|
|
19
|
+
useUserStore().setClientIpInfo(data)
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error('[GEO] Client-side fetch error:', err)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme "Animations → Reveal sections on scroll".
|
|
3
|
+
*
|
|
4
|
+
* Sections fade and rise in as they enter the screen. CSS does the motion
|
|
5
|
+
* (osi-controls.css, under `.osi-reveal`); this only marks each section once
|
|
6
|
+
* it's in view. A MutationObserver picks up sections added later — client
|
|
7
|
+
* navigation, the dashboard adding one in the preview.
|
|
8
|
+
*
|
|
9
|
+
* Off unless the theme turns it on (the root gets `.osi-reveal`), and CSS
|
|
10
|
+
* skips it entirely for visitors who prefer reduced motion.
|
|
11
|
+
*/
|
|
12
|
+
export default defineNuxtPlugin(() => {
|
|
13
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
14
|
+
|
|
15
|
+
const seen = new WeakSet();
|
|
16
|
+
const io = new IntersectionObserver(
|
|
17
|
+
(entries) => {
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (!entry.isIntersecting) continue;
|
|
20
|
+
entry.target.classList.add("is-revealed");
|
|
21
|
+
io.unobserve(entry.target);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function scan(root = document) {
|
|
28
|
+
root.querySelectorAll?.(".osi-section").forEach((el) => {
|
|
29
|
+
if (seen.has(el)) return;
|
|
30
|
+
seen.add(el);
|
|
31
|
+
io.observe(el);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const start = () => {
|
|
36
|
+
scan();
|
|
37
|
+
new MutationObserver((mutations) => {
|
|
38
|
+
for (const m of mutations) {
|
|
39
|
+
m.addedNodes.forEach((node) => {
|
|
40
|
+
if (node.nodeType !== 1) return;
|
|
41
|
+
if (node.classList?.contains("osi-section") && !seen.has(node)) {
|
|
42
|
+
seen.add(node);
|
|
43
|
+
io.observe(node);
|
|
44
|
+
}
|
|
45
|
+
scan(node);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
|
|
52
|
+
else start();
|
|
53
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from '#app';
|
|
2
|
+
import Toast from 'vue-toastification';
|
|
3
|
+
import 'vue-toastification/dist/index.css';
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
6
|
+
nuxtApp.vueApp.use(Toast, {
|
|
7
|
+
position: 'top-center',
|
|
8
|
+
timeout: 3000
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { authBus } from "./AuthBus";
|
|
2
|
+
import { tokenService } from "./Authentication/TokenService";
|
|
3
|
+
|
|
4
|
+
function safeState(key, fallback) {
|
|
5
|
+
try {
|
|
6
|
+
return useState(key, () => fallback).value;
|
|
7
|
+
} catch {
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function safeHost() {
|
|
13
|
+
try {
|
|
14
|
+
if (import.meta.server) {
|
|
15
|
+
const headers = useRequestHeaders(["host", "x-forwarded-host"]);
|
|
16
|
+
return headers["x-forwarded-host"] || headers.host || "";
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
return typeof location !== "undefined" ? location.host : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getBaseUrl() {
|
|
24
|
+
const fromState = safeState("base-url", "");
|
|
25
|
+
if (fromState) return fromState;
|
|
26
|
+
|
|
27
|
+
const host = safeHost();
|
|
28
|
+
return host ? `https://${host}/` : "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export const apiService = async (url, options = {}) => {
|
|
33
|
+
const { host: hostOption, ...fetchOptions } = options;
|
|
34
|
+
|
|
35
|
+
let token = tokenService.getToken();
|
|
36
|
+
const host = hostOption ?? safeHost();
|
|
37
|
+
|
|
38
|
+
const headers = {
|
|
39
|
+
...fetchOptions.headers,
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
// ...(host && { "X-Osimart-Store": host }),
|
|
42
|
+
...(token && { Authorization: `Bearer ${token}` }),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let response = await fetch(url, { ...fetchOptions, headers });
|
|
46
|
+
|
|
47
|
+
// Handle 401 error
|
|
48
|
+
if (response.status === 401 && token && !fetchOptions.skipAuthRefresh) {
|
|
49
|
+
console.warn("401 detected, queuing request for refresh...");
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
authBus.failedRequests.push({ url, options, resolve, reject });
|
|
53
|
+
|
|
54
|
+
if (!authBus.isRefreshing) {
|
|
55
|
+
authBus.refreshToken().then(() => {
|
|
56
|
+
authBus.retryFailedRequests();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
64
|
+
const err = new Error(
|
|
65
|
+
errorBody.detail || errorBody.message || `HTTP ${response.status}`
|
|
66
|
+
);
|
|
67
|
+
err.status = response.status;
|
|
68
|
+
err.data = errorBody;
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (response.status === 204) return null;
|
|
73
|
+
return response.json();
|
|
74
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { reactive, watch } from "vue";
|
|
2
|
+
import { tokenService } from "./Authentication/TokenService";
|
|
3
|
+
import UserServices from "./UserServices";
|
|
4
|
+
import { eventBus } from "../utils/eventBus";
|
|
5
|
+
import { useToast } from "vue-toastification";
|
|
6
|
+
import { navigateTo } from "nuxt/app";
|
|
7
|
+
import AuthenticationService from "./Authentication/AuthenticationService";
|
|
8
|
+
import { getBaseUrl } from "./ApiServices";
|
|
9
|
+
|
|
10
|
+
let hasAlerted = false;
|
|
11
|
+
|
|
12
|
+
export const authBus = reactive({
|
|
13
|
+
isLoggedIn: !!tokenService.getToken(),
|
|
14
|
+
userInfo: null,
|
|
15
|
+
isRefreshing: false,
|
|
16
|
+
failedRequests: [],
|
|
17
|
+
isLoggingOut: false,
|
|
18
|
+
|
|
19
|
+
async checkAuth() {
|
|
20
|
+
const token = tokenService.getToken();
|
|
21
|
+
this.isLoggedIn = !!token;
|
|
22
|
+
|
|
23
|
+
if (!this.isLoggedIn) {
|
|
24
|
+
this.userInfo = null;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
this.userInfo = await UserServices.getUserDetail(token);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error.status === 401) {
|
|
32
|
+
return this.handle401();
|
|
33
|
+
}
|
|
34
|
+
console.error("Auth check failed:", error);
|
|
35
|
+
await this.logout();
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async handle401() {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
this.failedRequests.push({ resolve, reject });
|
|
42
|
+
if (!this.isRefreshing) {
|
|
43
|
+
this.refreshToken().then(resolve).catch(reject);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async refreshToken() {
|
|
49
|
+
if (this.isRefreshing) return;
|
|
50
|
+
|
|
51
|
+
this.isRefreshing = true;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const refreshToken = tokenService.getRefreshToken();
|
|
55
|
+
if (!refreshToken) throw new Error("No refresh token available");
|
|
56
|
+
|
|
57
|
+
const baseUrl = getBaseUrl();
|
|
58
|
+
const response = await fetch(`${baseUrl}auth/refresh/`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
Authorization: `Bearer ${refreshToken}`,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error("Refresh failed");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = await response.json();
|
|
71
|
+
tokenService.setToken(data.access_token);
|
|
72
|
+
tokenService.setRefreshToken(data.refresh_token);
|
|
73
|
+
|
|
74
|
+
await this.retryFailedRequests();
|
|
75
|
+
|
|
76
|
+
return data.access_token;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
await this.logout();
|
|
79
|
+
|
|
80
|
+
if (!hasAlerted) {
|
|
81
|
+
hasAlerted = true;
|
|
82
|
+
try {
|
|
83
|
+
const toast = useToast();
|
|
84
|
+
toast.error("Session expired. Please log in again.");
|
|
85
|
+
} catch {
|
|
86
|
+
/* ignore toast error if outside vue context */
|
|
87
|
+
}
|
|
88
|
+
navigateTo("/login");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw error;
|
|
92
|
+
} finally {
|
|
93
|
+
this.isRefreshing = false;
|
|
94
|
+
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
hasAlerted = false;
|
|
97
|
+
}, 1000);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async retryFailedRequests() {
|
|
102
|
+
if (this.failedRequests.length === 0) return;
|
|
103
|
+
|
|
104
|
+
const newToken = tokenService.getToken();
|
|
105
|
+
|
|
106
|
+
for (const { url, options, resolve, reject } of this.failedRequests) {
|
|
107
|
+
const updatedOptions = {
|
|
108
|
+
...options,
|
|
109
|
+
headers: {
|
|
110
|
+
...options.headers,
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
Authorization: `Bearer ${newToken}`,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(url, updatedOptions);
|
|
118
|
+
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
121
|
+
const err = new Error(errorBody.detail || errorBody.message || `HTTP ${response.status}`);
|
|
122
|
+
err.status = response.status;
|
|
123
|
+
err.data = errorBody;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (response.status === 204) {
|
|
128
|
+
resolve(null);
|
|
129
|
+
} else {
|
|
130
|
+
resolve(await response.json());
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
reject(error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.failedRequests = [];
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async logout() {
|
|
141
|
+
if (this.isLoggingOut || !this.isLoggedIn) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.isLoggingOut = true;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await AuthenticationService.logout({ session_id: tokenService.getSessionId() });
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Logout request failed, clearing local session anyway:', error);
|
|
150
|
+
} finally {
|
|
151
|
+
tokenService.removeToken();
|
|
152
|
+
tokenService.removeRefreshToken();
|
|
153
|
+
tokenService.removeSessionId();
|
|
154
|
+
if (typeof localStorage !== 'undefined') localStorage.clear();
|
|
155
|
+
this.isLoggedIn = false;
|
|
156
|
+
this.userInfo = null;
|
|
157
|
+
eventBus.emit("cartUpdated");
|
|
158
|
+
eventBus.emit("authChanged", { isLoggedIn: false });
|
|
159
|
+
this.isLoggingOut = false;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { apiService, getBaseUrl } from '../ApiServices';
|
|
2
|
+
import { tokenService } from './TokenService';
|
|
3
|
+
import { eventBus } from '../../utils/eventBus';
|
|
4
|
+
|
|
5
|
+
const login = async (data) => {
|
|
6
|
+
try {
|
|
7
|
+
const baseUrl = getBaseUrl();
|
|
8
|
+
const responseData = await apiService(`${baseUrl}auth/login/`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
body: JSON.stringify(data),
|
|
11
|
+
});
|
|
12
|
+
tokenService.setToken(responseData.access_token);
|
|
13
|
+
tokenService.setRefreshToken(responseData.refresh_token);
|
|
14
|
+
tokenService.setSessionId(responseData.session_id);
|
|
15
|
+
eventBus.emit("authChanged", { isLoggedIn: true });
|
|
16
|
+
return responseData;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const register = async (data) => {
|
|
23
|
+
try {
|
|
24
|
+
const baseUrl = getBaseUrl();
|
|
25
|
+
const responseData = await apiService(`${baseUrl}auth/register/`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
body: JSON.stringify(data),
|
|
28
|
+
});
|
|
29
|
+
return responseData;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const forgotPassword = async (email) => {
|
|
36
|
+
try {
|
|
37
|
+
const baseUrl = getBaseUrl();
|
|
38
|
+
const responseData = await apiService(`${baseUrl}auth/forgot-password/`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
body: JSON.stringify({ email, reset_as: 'customer' })
|
|
41
|
+
});
|
|
42
|
+
return responseData;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const resetPassword = async (email, code, password) => {
|
|
49
|
+
try {
|
|
50
|
+
const baseUrl = getBaseUrl();
|
|
51
|
+
const responseData = await apiService(`${baseUrl}auth/reset-password/`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
body: JSON.stringify({ email, code, password, reset_as: 'customer' }),
|
|
54
|
+
});
|
|
55
|
+
return responseData;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const changePassword = async (data) => {
|
|
62
|
+
try {
|
|
63
|
+
const baseUrl = getBaseUrl();
|
|
64
|
+
const responseData = await apiService(`${baseUrl}auth/change-password/`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: JSON.stringify(data),
|
|
67
|
+
});
|
|
68
|
+
return responseData;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const verificationCode = async (email, code, deviceInfo = {}) => {
|
|
75
|
+
try {
|
|
76
|
+
const baseUrl = getBaseUrl();
|
|
77
|
+
const responseData = await apiService(`${baseUrl}auth/verify/`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
email,
|
|
81
|
+
code,
|
|
82
|
+
verify_as: 'customer',
|
|
83
|
+
device_id: deviceInfo.device_id,
|
|
84
|
+
device_name: deviceInfo.device_name,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
tokenService.setToken(responseData.access_token);
|
|
88
|
+
tokenService.setSessionId(responseData.session_id);
|
|
89
|
+
tokenService.setRefreshToken(responseData.refresh_token);
|
|
90
|
+
eventBus.emit("authChanged", { isLoggedIn: true });
|
|
91
|
+
return responseData;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const resendVerificationCode = async (email) => {
|
|
98
|
+
try {
|
|
99
|
+
const baseUrl = getBaseUrl();
|
|
100
|
+
const responseData = await apiService(`${baseUrl}auth/regen/`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: JSON.stringify({ email, verify_as: 'customer' }),
|
|
103
|
+
});
|
|
104
|
+
return responseData;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const logout = async (data) => {
|
|
111
|
+
try {
|
|
112
|
+
const baseUrl = getBaseUrl();
|
|
113
|
+
const responseData = await apiService(`${baseUrl}auth/logout/`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
body: JSON.stringify({ ...data }),
|
|
116
|
+
skipAuthRefresh: true,
|
|
117
|
+
});
|
|
118
|
+
return responseData;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default {
|
|
125
|
+
login,
|
|
126
|
+
register,
|
|
127
|
+
resendVerificationCode,
|
|
128
|
+
verificationCode,
|
|
129
|
+
forgotPassword,
|
|
130
|
+
resetPassword,
|
|
131
|
+
changePassword,
|
|
132
|
+
logout,
|
|
133
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import Cookies from "js-cookie";
|
|
2
|
+
import { eventBus } from "../../utils/eventBus";
|
|
3
|
+
|
|
4
|
+
export const tokenService = {
|
|
5
|
+
setToken(token) {
|
|
6
|
+
Cookies.set("token", token, { expires: 7 });
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
setSessionId(id) {
|
|
10
|
+
Cookies.set("session_id", id, { expires: 7 });
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
setRefreshToken(refreshToken) {
|
|
14
|
+
Cookies.set("refresh_token", refreshToken, { expires: 7 });
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
getRefreshToken() {
|
|
18
|
+
return Cookies.get("refresh_token");
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
removeRefreshToken() {
|
|
22
|
+
Cookies.remove("refresh_token");
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
getToken() {
|
|
26
|
+
return Cookies.get("token");
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
getSessionId() {
|
|
30
|
+
return Cookies.get("session_id");
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
removeToken() {
|
|
34
|
+
Cookies.remove("token");
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
removeSessionId() {
|
|
38
|
+
Cookies.remove("session_id");
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
setExpiration(expiresIn) {
|
|
42
|
+
const expiration = Date.now() + expiresIn * 1000;
|
|
43
|
+
Cookies.set("tokenExpiration", expiration);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
getExpiration() {
|
|
47
|
+
return parseInt(Cookies.get("tokenExpiration")) || 0;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
isTokenValid() {
|
|
51
|
+
return Date.now() < this.getExpiration();
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
isAuthenticated() {
|
|
55
|
+
return !!this.getToken();
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
clearCart() {
|
|
59
|
+
Cookies.remove("cart");
|
|
60
|
+
eventBus.emit("cartUpdated");
|
|
61
|
+
},
|
|
62
|
+
};
|