| |
| |
| |
| |
|
|
| import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' |
| import { useAuthStore } from '@/stores/auth' |
| import { useAppStore } from '@/stores/app' |
| import { useAdminSettingsStore } from '@/stores/adminSettings' |
| import { useNavigationLoadingState } from '@/composables/useNavigationLoading' |
| import { useRoutePrefetch } from '@/composables/useRoutePrefetch' |
| import { resolveDocumentTitle } from './title' |
|
|
| |
| |
| |
| const routes: RouteRecordRaw[] = [ |
| |
| { |
| path: '/setup', |
| name: 'Setup', |
| component: () => import('@/views/setup/SetupWizardView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'Setup' |
| } |
| }, |
|
|
| |
| { |
| path: '/home', |
| name: 'Home', |
| component: () => import('@/views/HomeView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'Home' |
| } |
| }, |
| { |
| path: '/login', |
| name: 'Login', |
| component: () => import('@/views/auth/LoginView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'Login', |
| titleKey: 'common.login' |
| } |
| }, |
| { |
| path: '/register', |
| name: 'Register', |
| component: () => import('@/views/auth/RegisterView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'Register', |
| titleKey: 'auth.createAccount' |
| } |
| }, |
| { |
| path: '/email-verify', |
| name: 'EmailVerify', |
| component: () => import('@/views/auth/EmailVerifyView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'Verify Email' |
| } |
| }, |
| { |
| path: '/auth/callback', |
| name: 'OAuthCallback', |
| component: () => import('@/views/auth/OAuthCallbackView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'OAuth Callback' |
| } |
| }, |
| { |
| path: '/auth/linuxdo/callback', |
| name: 'LinuxDoOAuthCallback', |
| component: () => import('@/views/auth/LinuxDoCallbackView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'LinuxDo OAuth Callback' |
| } |
| }, |
| { |
| path: '/forgot-password', |
| name: 'ForgotPassword', |
| component: () => import('@/views/auth/ForgotPasswordView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'Forgot Password', |
| titleKey: 'auth.forgotPasswordTitle' |
| } |
| }, |
| { |
| path: '/reset-password', |
| name: 'ResetPassword', |
| component: () => import('@/views/auth/ResetPasswordView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'Reset Password' |
| } |
| }, |
| { |
| path: '/key-usage', |
| name: 'KeyUsage', |
| component: () => import('@/views/KeyUsageView.vue'), |
| meta: { |
| requiresAuth: false, |
| title: 'Key Usage', |
| } |
| }, |
|
|
| |
| { |
| path: '/', |
| redirect: '/login' |
| }, |
| { |
| path: '/dashboard', |
| name: 'Dashboard', |
| component: () => import('@/views/user/DashboardView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'Dashboard', |
| titleKey: 'dashboard.title', |
| descriptionKey: 'dashboard.welcomeMessage' |
| } |
| }, |
| { |
| path: '/keys', |
| name: 'Keys', |
| component: () => import('@/views/user/KeysView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'API Keys', |
| titleKey: 'keys.title', |
| descriptionKey: 'keys.description' |
| } |
| }, |
| { |
| path: '/usage', |
| name: 'Usage', |
| component: () => import('@/views/user/UsageView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'Usage Records', |
| titleKey: 'usage.title', |
| descriptionKey: 'usage.description' |
| } |
| }, |
| { |
| path: '/redeem', |
| name: 'Redeem', |
| component: () => import('@/views/user/RedeemView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'Redeem Code', |
| titleKey: 'redeem.title', |
| descriptionKey: 'redeem.description' |
| } |
| }, |
| { |
| path: '/profile', |
| name: 'Profile', |
| component: () => import('@/views/user/ProfileView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'Profile', |
| titleKey: 'profile.title', |
| descriptionKey: 'profile.description' |
| } |
| }, |
| { |
| path: '/subscriptions', |
| name: 'Subscriptions', |
| component: () => import('@/views/user/SubscriptionsView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'My Subscriptions', |
| titleKey: 'userSubscriptions.title', |
| descriptionKey: 'userSubscriptions.description' |
| } |
| }, |
| { |
| path: '/purchase', |
| name: 'PurchaseSubscription', |
| component: () => import('@/views/user/PurchaseSubscriptionView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'Purchase Subscription', |
| titleKey: 'purchase.title', |
| descriptionKey: 'purchase.description' |
| } |
| }, |
| { |
| path: '/sora', |
| name: 'Sora', |
| component: () => import('@/views/user/SoraView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'Sora', |
| titleKey: 'sora.title', |
| descriptionKey: 'sora.description' |
| } |
| }, |
| { |
| path: '/custom/:id', |
| name: 'CustomPage', |
| component: () => import('@/views/user/CustomPageView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: false, |
| title: 'Custom Page', |
| titleKey: 'customPage.title', |
| } |
| }, |
|
|
| |
| { |
| path: '/admin', |
| redirect: '/admin/dashboard' |
| }, |
| { |
| path: '/admin/dashboard', |
| name: 'AdminDashboard', |
| component: () => import('@/views/admin/DashboardView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Admin Dashboard', |
| titleKey: 'admin.dashboard.title', |
| descriptionKey: 'admin.dashboard.description' |
| } |
| }, |
| { |
| path: '/admin/ops', |
| name: 'AdminOps', |
| component: () => import('@/views/admin/ops/OpsDashboard.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Ops Monitoring', |
| titleKey: 'admin.ops.title', |
| descriptionKey: 'admin.ops.description' |
| } |
| }, |
| { |
| path: '/admin/users', |
| name: 'AdminUsers', |
| component: () => import('@/views/admin/UsersView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'User Management', |
| titleKey: 'admin.users.title', |
| descriptionKey: 'admin.users.description' |
| } |
| }, |
| { |
| path: '/admin/groups', |
| name: 'AdminGroups', |
| component: () => import('@/views/admin/GroupsView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Group Management', |
| titleKey: 'admin.groups.title', |
| descriptionKey: 'admin.groups.description' |
| } |
| }, |
| { |
| path: '/admin/subscriptions', |
| name: 'AdminSubscriptions', |
| component: () => import('@/views/admin/SubscriptionsView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Subscription Management', |
| titleKey: 'admin.subscriptions.title', |
| descriptionKey: 'admin.subscriptions.description' |
| } |
| }, |
| { |
| path: '/admin/accounts', |
| name: 'AdminAccounts', |
| component: () => import('@/views/admin/AccountsView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Account Management', |
| titleKey: 'admin.accounts.title', |
| descriptionKey: 'admin.accounts.description' |
| } |
| }, |
| { |
| path: '/admin/announcements', |
| name: 'AdminAnnouncements', |
| component: () => import('@/views/admin/AnnouncementsView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Announcements', |
| titleKey: 'admin.announcements.title', |
| descriptionKey: 'admin.announcements.description' |
| } |
| }, |
| { |
| path: '/admin/proxies', |
| name: 'AdminProxies', |
| component: () => import('@/views/admin/ProxiesView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Proxy Management', |
| titleKey: 'admin.proxies.title', |
| descriptionKey: 'admin.proxies.description' |
| } |
| }, |
| { |
| path: '/admin/redeem', |
| name: 'AdminRedeem', |
| component: () => import('@/views/admin/RedeemView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Redeem Code Management', |
| titleKey: 'admin.redeem.title', |
| descriptionKey: 'admin.redeem.description' |
| } |
| }, |
| { |
| path: '/admin/promo-codes', |
| name: 'AdminPromoCodes', |
| component: () => import('@/views/admin/PromoCodesView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Promo Code Management', |
| titleKey: 'admin.promo.title', |
| descriptionKey: 'admin.promo.description' |
| } |
| }, |
| { |
| path: '/admin/settings', |
| name: 'AdminSettings', |
| component: () => import('@/views/admin/SettingsView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'System Settings', |
| titleKey: 'admin.settings.title', |
| descriptionKey: 'admin.settings.description' |
| } |
| }, |
| { |
| path: '/admin/usage', |
| name: 'AdminUsage', |
| component: () => import('@/views/admin/UsageView.vue'), |
| meta: { |
| requiresAuth: true, |
| requiresAdmin: true, |
| title: 'Usage Records', |
| titleKey: 'admin.usage.title', |
| descriptionKey: 'admin.usage.description' |
| } |
| }, |
|
|
| |
| { |
| path: '/:pathMatch(.*)*', |
| name: 'NotFound', |
| component: () => import('@/views/NotFoundView.vue'), |
| meta: { |
| title: '404 Not Found' |
| } |
| } |
| ] |
|
|
| |
| |
| |
| const router = createRouter({ |
| history: createWebHistory(import.meta.env.BASE_URL), |
| routes, |
| scrollBehavior(_to, _from, savedPosition) { |
| |
| if (savedPosition) { |
| return savedPosition |
| } |
| |
| return { top: 0 } |
| } |
| }) |
|
|
| |
| |
| |
| let authInitialized = false |
|
|
| |
| const navigationLoading = useNavigationLoadingState() |
| |
| let routePrefetch: ReturnType<typeof useRoutePrefetch> | null = null |
| const BACKEND_MODE_ALLOWED_PATHS = ['/login', '/key-usage', '/setup'] |
|
|
| router.beforeEach((to, _from, next) => { |
| |
| navigationLoading.startNavigation() |
|
|
| const authStore = useAuthStore() |
|
|
| |
| if (!authInitialized) { |
| authStore.checkAuth() |
| authInitialized = true |
| } |
|
|
| |
| const appStore = useAppStore() |
| |
| if (to.name === 'CustomPage') { |
| const id = to.params.id as string |
| const publicItems = appStore.cachedPublicSettings?.custom_menu_items ?? [] |
| const adminSettingsStore = useAdminSettingsStore() |
| const menuItem = publicItems.find((item) => item.id === id) |
| ?? (authStore.isAdmin ? adminSettingsStore.customMenuItems.find((item) => item.id === id) : undefined) |
| if (menuItem?.label) { |
| const siteName = appStore.siteName || 'Sub2API' |
| document.title = `${menuItem.label} - ${siteName}` |
| } else { |
| document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string) |
| } |
| } else { |
| document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string) |
| } |
|
|
| |
| const requiresAuth = to.meta.requiresAuth !== false |
| const requiresAdmin = to.meta.requiresAdmin === true |
|
|
| |
| if (!requiresAuth) { |
| |
| if (authStore.isAuthenticated && (to.path === '/login' || to.path === '/register')) { |
| |
| |
| if (appStore.backendModeEnabled && !authStore.isAdmin) { |
| next() |
| return |
| } |
| |
| next(authStore.isAdmin ? '/admin/dashboard' : '/dashboard') |
| return |
| } |
| |
| if (appStore.backendModeEnabled && !authStore.isAuthenticated) { |
| const isAllowed = BACKEND_MODE_ALLOWED_PATHS.some((p) => to.path === p || to.path.startsWith(p)) |
| if (!isAllowed) { |
| next('/login') |
| return |
| } |
| } |
| next() |
| return |
| } |
|
|
| |
| if (!authStore.isAuthenticated) { |
| |
| next({ |
| path: '/login', |
| query: { redirect: to.fullPath } |
| }) |
| return |
| } |
|
|
| |
| if (requiresAdmin && !authStore.isAdmin) { |
| |
| next('/dashboard') |
| return |
| } |
|
|
| |
| if (authStore.isSimpleMode) { |
| const restrictedPaths = [ |
| '/admin/groups', |
| '/admin/subscriptions', |
| '/admin/redeem', |
| '/subscriptions', |
| '/redeem' |
| ] |
|
|
| if (restrictedPaths.some((path) => to.path.startsWith(path))) { |
| |
| next(authStore.isAdmin ? '/admin/dashboard' : '/dashboard') |
| return |
| } |
| } |
|
|
| |
| if (appStore.backendModeEnabled) { |
| if (authStore.isAuthenticated && authStore.isAdmin) { |
| next() |
| return |
| } |
| const isAllowed = BACKEND_MODE_ALLOWED_PATHS.some((p) => to.path === p || to.path.startsWith(p)) |
| if (!isAllowed) { |
| next('/login') |
| return |
| } |
| } |
|
|
| |
| next() |
| }) |
|
|
| |
| |
| |
| router.afterEach((to) => { |
| |
| navigationLoading.endNavigation() |
|
|
| |
| if (!routePrefetch) { |
| routePrefetch = useRoutePrefetch(router) |
| } |
| |
| routePrefetch.triggerPrefetch(to) |
| }) |
|
|
| |
| |
| |
| |
| router.onError((error) => { |
| console.error('Router error:', error) |
|
|
| |
| const isChunkLoadError = |
| error.message?.includes('Failed to fetch dynamically imported module') || |
| error.message?.includes('Loading chunk') || |
| error.message?.includes('Loading CSS chunk') || |
| error.name === 'ChunkLoadError' |
|
|
| if (isChunkLoadError) { |
| |
| const reloadKey = 'chunk_reload_attempted' |
| const lastReload = sessionStorage.getItem(reloadKey) |
| const now = Date.now() |
|
|
| |
| if (!lastReload || now - parseInt(lastReload) > 10000) { |
| sessionStorage.setItem(reloadKey, now.toString()) |
| console.warn('Chunk load error detected, reloading page to fetch latest version...') |
| window.location.reload() |
| } else { |
| console.error('Chunk load error persists after reload. Please clear browser cache.') |
| } |
| } |
| }) |
|
|
| export default router |
|
|