First commit
This commit is contained in:
273
frontend/src/router/README.md
Normal file
273
frontend/src/router/README.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Vue Router Configuration
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains the Vue Router configuration for the Sub2API frontend application. The router implements a comprehensive navigation system with authentication guards, role-based access control, and lazy loading.
|
||||
|
||||
## Files
|
||||
|
||||
- **index.ts**: Main router configuration with route definitions and navigation guards
|
||||
- **meta.d.ts**: TypeScript type definitions for route meta fields
|
||||
|
||||
## Route Structure
|
||||
|
||||
### Public Routes (No Authentication Required)
|
||||
|
||||
| Path | Component | Description |
|
||||
|------|-----------|-------------|
|
||||
| `/login` | LoginView | User login page |
|
||||
| `/register` | RegisterView | User registration page |
|
||||
|
||||
### User Routes (Authentication Required)
|
||||
|
||||
| Path | Component | Description |
|
||||
|------|-----------|-------------|
|
||||
| `/` | - | Redirects to `/dashboard` |
|
||||
| `/dashboard` | DashboardView | User dashboard with stats |
|
||||
| `/keys` | KeysView | API key management |
|
||||
| `/usage` | UsageView | Usage records and statistics |
|
||||
| `/redeem` | RedeemView | Redeem code interface |
|
||||
| `/profile` | ProfileView | User profile settings |
|
||||
|
||||
### Admin Routes (Admin Role Required)
|
||||
|
||||
| Path | Component | Description |
|
||||
|------|-----------|-------------|
|
||||
| `/admin` | - | Redirects to `/admin/dashboard` |
|
||||
| `/admin/dashboard` | AdminDashboardView | Admin dashboard |
|
||||
| `/admin/users` | AdminUsersView | User management |
|
||||
| `/admin/groups` | AdminGroupsView | Group management |
|
||||
| `/admin/accounts` | AdminAccountsView | Account management |
|
||||
| `/admin/proxies` | AdminProxiesView | Proxy management |
|
||||
| `/admin/redeem` | AdminRedeemView | Redeem code management |
|
||||
|
||||
### Special Routes
|
||||
|
||||
| Path | Component | Description |
|
||||
|------|-----------|-------------|
|
||||
| `/:pathMatch(.*)` | NotFoundView | 404 error page |
|
||||
|
||||
## Navigation Guards
|
||||
|
||||
### Authentication Guard (beforeEach)
|
||||
|
||||
The router implements a comprehensive navigation guard that:
|
||||
|
||||
1. **Sets Page Title**: Updates document title based on route meta
|
||||
2. **Checks Authentication**:
|
||||
- Public routes (`requiresAuth: false`) are accessible without login
|
||||
- Protected routes require authentication
|
||||
- Redirects to `/login` if not authenticated
|
||||
3. **Prevents Double Login**:
|
||||
- Redirects authenticated users away from login/register pages
|
||||
4. **Role-Based Access Control**:
|
||||
- Admin routes (`requiresAdmin: true`) require admin role
|
||||
- Non-admin users are redirected to `/dashboard`
|
||||
5. **Preserves Intended Destination**:
|
||||
- Saves original URL in query parameter for post-login redirect
|
||||
|
||||
### Flow Diagram
|
||||
|
||||
```
|
||||
User navigates to route
|
||||
↓
|
||||
Set page title from meta
|
||||
↓
|
||||
Is route public? ──Yes──→ Already authenticated? ──Yes──→ Redirect to /dashboard
|
||||
↓ No ↓ No
|
||||
↓ Allow access
|
||||
↓
|
||||
Is user authenticated? ──No──→ Redirect to /login with redirect query
|
||||
↓ Yes
|
||||
↓
|
||||
Requires admin role? ──Yes──→ Is user admin? ──No──→ Redirect to /dashboard
|
||||
↓ No ↓ Yes
|
||||
↓ ↓
|
||||
Allow access ←────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Route Meta Fields
|
||||
|
||||
Each route can define the following meta fields:
|
||||
|
||||
```typescript
|
||||
interface RouteMeta {
|
||||
requiresAuth?: boolean; // Default: true (requires authentication)
|
||||
requiresAdmin?: boolean; // Default: false (admin access only)
|
||||
title?: string; // Page title
|
||||
breadcrumbs?: Array<{ // Breadcrumb navigation
|
||||
label: string;
|
||||
to?: string;
|
||||
}>;
|
||||
icon?: string; // Icon for navigation menu
|
||||
hideInMenu?: boolean; // Hide from navigation menu
|
||||
}
|
||||
```
|
||||
|
||||
## Lazy Loading
|
||||
|
||||
All route components use dynamic imports for code splitting:
|
||||
|
||||
```typescript
|
||||
component: () => import('@/views/user/DashboardView.vue')
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Reduced initial bundle size
|
||||
- Faster initial page load
|
||||
- Components loaded on-demand
|
||||
- Automatic code splitting by Vite
|
||||
|
||||
## Authentication Store Integration
|
||||
|
||||
The router integrates with the Pinia auth store (`@/stores/auth`):
|
||||
|
||||
```typescript
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// Check authentication status
|
||||
authStore.isAuthenticated
|
||||
|
||||
// Check admin role
|
||||
authStore.isAdmin
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
```typescript
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Navigate to a route
|
||||
router.push('/dashboard');
|
||||
|
||||
// Navigate with query parameters
|
||||
router.push({
|
||||
path: '/usage',
|
||||
query: { filter: 'today' }
|
||||
});
|
||||
|
||||
// Navigate to admin route (will be blocked if not admin)
|
||||
router.push('/admin/users');
|
||||
```
|
||||
|
||||
### Route Links
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Simple link -->
|
||||
<router-link to="/dashboard">Dashboard</router-link>
|
||||
|
||||
<!-- Named route -->
|
||||
<router-link :to="{ name: 'Keys' }">API Keys</router-link>
|
||||
|
||||
<!-- With query parameters -->
|
||||
<router-link :to="{ path: '/usage', query: { page: 1 } }">
|
||||
Usage
|
||||
</router-link>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Checking Current Route
|
||||
|
||||
```typescript
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// Check if on admin page
|
||||
const isAdminPage = route.path.startsWith('/admin');
|
||||
|
||||
// Get route meta
|
||||
const requiresAdmin = route.meta.requiresAdmin;
|
||||
```
|
||||
|
||||
## Scroll Behavior
|
||||
|
||||
The router implements automatic scroll management:
|
||||
|
||||
- **Browser Navigation**: Restores saved scroll position
|
||||
- **New Routes**: Scrolls to top of page
|
||||
- **Hash Links**: Scrolls to anchor (when implemented)
|
||||
|
||||
## Error Handling
|
||||
|
||||
The router includes error handling for navigation failures:
|
||||
|
||||
```typescript
|
||||
router.onError((error) => {
|
||||
console.error('Router error:', error);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Routes
|
||||
|
||||
To test navigation guards and route access:
|
||||
|
||||
1. **Public Route Access**: Visit `/login` without authentication
|
||||
2. **Protected Route**: Try accessing `/dashboard` without login (should redirect)
|
||||
3. **Admin Access**: Login as regular user, try `/admin/users` (should redirect to dashboard)
|
||||
4. **Admin Success**: Login as admin, access `/admin/users` (should succeed)
|
||||
5. **404 Handling**: Visit non-existent route (should show 404 page)
|
||||
|
||||
## Development Tips
|
||||
|
||||
### Adding New Routes
|
||||
|
||||
1. Add route definition in `routes` array
|
||||
2. Create corresponding view component
|
||||
3. Set appropriate meta fields (`requiresAuth`, `requiresAdmin`)
|
||||
4. Use lazy loading with `() => import()`
|
||||
5. Update this README with route documentation
|
||||
|
||||
### Debugging Navigation
|
||||
|
||||
Enable Vue Router debug mode:
|
||||
|
||||
```typescript
|
||||
// In browser console
|
||||
window.__VUE_ROUTER__ = router;
|
||||
|
||||
// Check current route
|
||||
router.currentRoute.value
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue**: 404 on page refresh
|
||||
- **Cause**: Server not configured for SPA
|
||||
- **Solution**: Configure server to serve `index.html` for all routes
|
||||
|
||||
**Issue**: Navigation guard runs twice
|
||||
- **Cause**: Multiple `next()` calls
|
||||
- **Solution**: Ensure only one `next()` call per code path
|
||||
|
||||
**Issue**: User data not loaded
|
||||
- **Cause**: Auth store not initialized
|
||||
- **Solution**: Call `authStore.checkAuth()` in App.vue or main.ts
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Client-Side Only**: Navigation guards are client-side; server must also validate
|
||||
2. **Token Validation**: API should verify JWT token on every request
|
||||
3. **Role Checking**: Backend must verify admin role, not just frontend
|
||||
4. **XSS Protection**: Vue automatically escapes template content
|
||||
5. **CSRF Protection**: Use CSRF tokens for state-changing operations
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
1. **Lazy Loading**: All routes use dynamic imports
|
||||
2. **Code Splitting**: Vite automatically splits route chunks
|
||||
3. **Prefetching**: Consider adding route prefetch for common paths
|
||||
4. **Route Caching**: Vue Router caches component instances
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Add breadcrumb navigation system
|
||||
- [ ] Implement route-based permissions beyond admin/user
|
||||
- [ ] Add route transition animations
|
||||
- [ ] Implement route prefetching for anticipated navigation
|
||||
- [ ] Add navigation analytics tracking
|
||||
345
frontend/src/router/index.ts
Normal file
345
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* Vue Router configuration for Sub2API frontend
|
||||
* Defines all application routes with lazy loading and navigation guards
|
||||
*/
|
||||
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
/**
|
||||
* Route definitions with lazy loading
|
||||
*/
|
||||
const routes: RouteRecordRaw[] = [
|
||||
// ==================== Setup Routes ====================
|
||||
{
|
||||
path: '/setup',
|
||||
name: 'Setup',
|
||||
component: () => import('@/views/setup/SetupWizardView.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: 'Setup',
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Public Routes ====================
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('@/views/auth/RegisterView.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: 'Register',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/email-verify',
|
||||
name: 'EmailVerify',
|
||||
component: () => import('@/views/auth/EmailVerifyView.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: 'Verify Email',
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== User Routes ====================
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/home',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Admin Routes ====================
|
||||
{
|
||||
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/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/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/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',
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== 404 Not Found ====================
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/NotFoundView.vue'),
|
||||
meta: {
|
||||
title: '404 Not Found',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Create router instance
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// Scroll to saved position when using browser back/forward
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
// Scroll to top for new routes
|
||||
return { top: 0 };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Navigation guard: Authentication check
|
||||
*/
|
||||
let authInitialized = false;
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// Restore auth state from localStorage on first navigation (page refresh)
|
||||
if (!authInitialized) {
|
||||
authStore.checkAuth();
|
||||
authInitialized = true;
|
||||
}
|
||||
|
||||
// Set page title
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - Sub2API`;
|
||||
} else {
|
||||
document.title = 'Sub2API';
|
||||
}
|
||||
|
||||
// Check if route requires authentication
|
||||
const requiresAuth = to.meta.requiresAuth !== false; // Default to true
|
||||
const requiresAdmin = to.meta.requiresAdmin === true;
|
||||
|
||||
// If route doesn't require auth, allow access
|
||||
if (!requiresAuth) {
|
||||
// If already authenticated and trying to access login/register, redirect to dashboard
|
||||
if (authStore.isAuthenticated && (to.path === '/login' || to.path === '/register')) {
|
||||
next('/dashboard');
|
||||
return;
|
||||
}
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Route requires authentication
|
||||
if (!authStore.isAuthenticated) {
|
||||
// Not authenticated, redirect to login
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }, // Save intended destination
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check admin requirement
|
||||
if (requiresAdmin && !authStore.isAdmin) {
|
||||
// User is authenticated but not admin, redirect to user dashboard
|
||||
next('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
// All checks passed, allow navigation
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Navigation guard: Error handling
|
||||
*/
|
||||
router.onError((error) => {
|
||||
console.error('Router error:', error);
|
||||
});
|
||||
|
||||
export default router;
|
||||
46
frontend/src/router/meta.d.ts
vendored
Normal file
46
frontend/src/router/meta.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Type definitions for Vue Router meta fields
|
||||
* Extends the RouteMeta interface with custom properties
|
||||
*/
|
||||
|
||||
import 'vue-router';
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
/**
|
||||
* Whether this route requires authentication
|
||||
* @default true
|
||||
*/
|
||||
requiresAuth?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this route requires admin role
|
||||
* @default false
|
||||
*/
|
||||
requiresAdmin?: boolean;
|
||||
|
||||
/**
|
||||
* Page title for this route
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Optional breadcrumb items for navigation
|
||||
*/
|
||||
breadcrumbs?: Array<{
|
||||
label: string;
|
||||
to?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Icon name for this route (for sidebar navigation)
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* Whether to hide this route from navigation menu
|
||||
* @default false
|
||||
*/
|
||||
hideInMenu?: boolean;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user