refactor(frontend): modularize i18n architecture

This commit is contained in:
elky
2026-06-30 17:01:39 +08:00
parent bf29129a4b
commit 9f138d09e6
118 changed files with 11940 additions and 5368 deletions
@@ -0,0 +1,53 @@
import { beforeEach, describe, expect, it } from 'vitest'
import type { RouteLocationNormalized } from 'vue-router'
import { resolveHomeRedirect } from '@/router/guards/homeGuard'
function route(path: string, query: Record<string, string> = {}): RouteLocationNormalized {
return {
path,
fullPath: path,
query,
hash: '',
name: undefined,
params: {},
matched: [],
meta: {},
redirectedFrom: undefined,
} as RouteLocationNormalized
}
function authStore(options: { isAuthenticated: boolean; canAccessAdmin?: boolean }) {
return {
isAuthenticated: options.isAuthenticated,
canAccessAdmin: options.canAccessAdmin ?? false,
} as never
}
describe('resolveHomeRedirect', () => {
beforeEach(() => {
sessionStorage.clear()
})
it('ignores non-home routes and unauthenticated home visits', () => {
expect(resolveHomeRedirect(route('/dashboard'), route('/'), authStore({ isAuthenticated: true }))).toBeNull()
expect(resolveHomeRedirect(route('/'), route('/login'), authStore({ isAuthenticated: false }))).toBeNull()
})
it('allows authenticated users to return to the public home from the app shell', () => {
expect(resolveHomeRedirect(route('/'), route('/dashboard'), authStore({ isAuthenticated: true }))).toBe('')
expect(resolveHomeRedirect(route('/', { returnTo: '/guide' }), route('/external'), authStore({ isAuthenticated: true }))).toBe('')
})
it('consumes stored redirect path before falling back to dashboard defaults', () => {
sessionStorage.setItem('redirectPath', '/dashboard/api-keys')
expect(resolveHomeRedirect(route('/'), route('/external'), authStore({ isAuthenticated: true }))).toBe('/dashboard/api-keys')
expect(sessionStorage.getItem('redirectPath')).toBeNull()
})
it('routes authenticated users to the correct dashboard by role', () => {
expect(resolveHomeRedirect(route('/'), route('/external'), authStore({ isAuthenticated: true }))).toBe('/dashboard')
expect(resolveHomeRedirect(route('/'), route('/external'), authStore({ isAuthenticated: true, canAccessAdmin: true }))).toBe('/admin/dashboard')
})
})
+1 -378
View File
@@ -1,8 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useModuleStore } from '@/stores/modules'
import { importWithRetry } from '@/utils/importRetry'
import { log } from '@/utils/logger'
import {
ensureUserLoaded,
@@ -10,382 +8,7 @@ import {
checkAdminAccess,
checkModuleAccess
} from './guards'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => importWithRetry(() => import('@/views/public/Home.vue')),
meta: { requiresAuth: false }
},
{
path: '/register',
name: 'RegisterEntry',
component: () => importWithRetry(() => import('@/views/public/Home.vue')),
meta: { requiresAuth: false }
},
{
path: '/privacy-policy',
name: 'PrivacyPolicy',
component: () => importWithRetry(() => import('@/views/public/PrivacyPolicy.vue')),
meta: { requiresAuth: false }
},
{
path: '/guide',
component: () => importWithRetry(() => import('@/views/public/guide/GuideLayout.vue')),
meta: { requiresAuth: false },
children: [
{
path: '',
name: 'GuideOverview',
component: () => importWithRetry(() => import('@/views/public/guide/Overview.vue')),
meta: { requiresAuth: false }
},
{
path: 'architecture',
name: 'GuideArchitecture',
component: () => importWithRetry(() => import('@/views/public/guide/ArchitectureGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'concepts',
name: 'GuideConcepts',
component: () => importWithRetry(() => import('@/views/public/guide/ConceptsGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'strategy',
name: 'GuideStrategy',
component: () => importWithRetry(() => import('@/views/public/guide/StrategyGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'advanced',
name: 'GuideAdvanced',
component: () => importWithRetry(() => import('@/views/public/guide/AdvancedGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'faq',
name: 'GuideFaq',
component: () => importWithRetry(() => import('@/views/public/guide/GuideFaq.vue')),
meta: { requiresAuth: false }
},
{
path: 'modules',
name: 'GuideModules',
component: () => importWithRetry(() => import('@/views/public/guide/ModulesGuide.vue')),
meta: { requiresAuth: false }
}
]
},
{
path: '/logo-demo',
name: 'LogoColorDemo',
component: () => importWithRetry(() => import('@/views/public/LogoColorDemo.vue')),
meta: { requiresAuth: false }
},
{
path: '/auth/callback',
name: 'AuthCallback',
component: () => importWithRetry(() => import('@/views/public/AuthCallback.vue')),
meta: { requiresAuth: false }
},
{
path: '/dashboard',
component: () => importWithRetry(() => import('@/layouts/MainLayout.vue')),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'Dashboard',
component: () => importWithRetry(() => import('@/views/shared/Dashboard.vue'))
},
{
path: 'api-keys',
name: 'MyApiKeys',
component: () => importWithRetry(() => import('@/views/user/MyApiKeys.vue'))
},
{
path: 'management-tokens',
name: 'ManagementTokens',
component: () => importWithRetry(() => import('@/views/user/ManagementTokens.vue')),
meta: { module: 'management_tokens' }
},
{
path: 'announcements',
name: 'Announcements',
component: () => importWithRetry(() => import('@/views/user/Announcements.vue'))
},
{
path: 'usage',
name: 'MyUsage',
component: () => importWithRetry(() => import('@/views/shared/Usage.vue'))
},
{
path: 'endpoint-status',
name: 'EndpointStatus',
component: () => importWithRetry(() => import('@/views/shared/HealthMonitor.vue'))
},
{
path: 'settings',
name: 'Settings',
component: () => importWithRetry(() => import('@/views/user/Settings.vue'))
},
{
path: 'wallet',
name: 'WalletCenter',
component: () => importWithRetry(() => import('@/views/user/WalletCenter.vue'))
},
{
path: 'billing',
name: 'BillingPlans',
component: () => importWithRetry(() => import('@/views/user/BillingPlans.vue'))
},
{
path: 'referral',
name: 'ReferralCenter',
component: () => importWithRetry(() => import('@/views/user/ReferralCenter.vue'))
},
{
path: 'models',
name: 'ModelCatalog',
component: () => importWithRetry(() => import('@/views/user/ModelCatalog.vue'))
},
{
path: 'async-tasks',
name: 'UserAsyncTasks',
component: () => importWithRetry(() => import('@/views/admin/AsyncTasks.vue'))
}
]
},
{
path: '/admin',
component: () => importWithRetry(() => import('@/layouts/MainLayout.vue')),
meta: { requiresAuth: true, requiresAdmin: true },
children: [
{
path: 'dashboard',
name: 'AdminDashboard',
component: () => importWithRetry(() => import('@/views/shared/Dashboard.vue'))
},
{
path: 'operations',
name: 'AdminOperationsDashboard',
component: () => importWithRetry(() => import('@/views/admin/AdminOperationsDashboard.vue'))
},
{
path: 'users',
name: 'Users',
component: () => importWithRetry(() => import('@/views/admin/Users.vue'))
},
{
path: 'keys',
name: 'ApiKeys',
component: () => importWithRetry(() => import('@/views/admin/ApiKeys.vue'))
},
{
path: 'wallets',
name: 'WalletsManagement',
component: () => importWithRetry(() => import('@/views/admin/WalletsManagement.vue'))
},
{
path: 'payment-gateways',
name: 'PaymentGatewaySettings',
component: () => importWithRetry(() => import('@/views/admin/PaymentGatewaySettings.vue'))
},
{
path: 'billing-plans',
name: 'BillingPlansManagement',
component: () => importWithRetry(() => import('@/views/admin/BillingPlansManagement.vue'))
},
{
path: 'referrals',
name: 'ReferralManagement',
component: () => importWithRetry(() => import('@/views/admin/ReferralManagement.vue'))
},
{
path: 'management-tokens',
name: 'AdminManagementTokens',
component: () => importWithRetry(() => import('@/views/user/ManagementTokens.vue')),
meta: { module: 'management_tokens' }
},
{
path: 'providers',
name: 'ProviderManagement',
component: () => importWithRetry(() => import('@/views/admin/ProviderManagement.vue'))
},
{
path: 'pool',
name: 'PoolManagement',
component: () => importWithRetry(() => import('@/views/admin/PoolManagement.vue'))
},
{
path: 'models',
name: 'ModelManagement',
component: () => importWithRetry(() => import('@/views/admin/ModelManagement.vue'))
},
{
path: 'routing',
name: 'RoutingProfiles',
component: () => importWithRetry(() => import('@/views/admin/RoutingProfiles.vue'))
},
{
path: 'routing/new',
name: 'RoutingProfileCreate',
component: () => importWithRetry(() => import('@/views/admin/RoutingProfiles.vue'))
},
{
path: 'routing/:groupId',
name: 'RoutingProfileDetail',
component: () => importWithRetry(() => import('@/views/admin/RoutingProfiles.vue'))
},
{
path: 'health-monitor',
name: 'HealthMonitor',
component: () => importWithRetry(() => import('@/views/shared/HealthMonitor.vue'))
},
{
path: 'usage',
name: 'Usage',
component: () => importWithRetry(() => import('@/views/shared/Usage.vue'))
},
{
path: 'user-stats',
name: 'UserStats',
component: () => importWithRetry(() => import('@/views/admin/UserStats.vue'))
},
{
path: 'cost-analysis',
name: 'CostAnalysis',
component: () => importWithRetry(() => import('@/views/admin/CostAnalysis.vue'))
},
{
path: 'performance-analysis',
name: 'PerformanceAnalysis',
component: () => importWithRetry(() => import('@/views/admin/PerformanceAnalysis.vue'))
},
{
path: 'system',
name: 'SystemSettings',
component: () => importWithRetry(() => import('@/views/admin/SystemSettings.vue'))
},
{
path: 'modules',
name: 'ModuleManagement',
component: () => importWithRetry(() => import('@/views/admin/ModuleManagement.vue'))
},
{
path: 'model-directives',
name: 'ModelDirectivesManagement',
component: () => importWithRetry(() => import('@/views/admin/ModelDirectivesManagement.vue')),
meta: { module: 'model_directives' }
},
{
path: 'modules/chat-pii-redaction',
name: 'ChatPiiRedactionModule',
component: () => importWithRetry(() => import('@/views/admin/modules/ChatPiiRedaction.vue')),
meta: { module: 'chat_pii_redaction' }
},
{
path: 'modules/s3-backup',
name: 'S3BackupSettings',
component: () => importWithRetry(() => import('@/views/admin/modules/S3BackupSettings.vue')),
meta: { module: 's3_backup' }
},
{
path: 'modules/important-notification',
redirect: '/admin/notification-service'
},
{
path: 'notification-service',
name: 'ImportantNotificationModule',
component: () => importWithRetry(() => import('@/views/admin/modules/ImportantNotification.vue')),
meta: { module: 'important_notification' }
},
{
path: 'server-chan',
redirect: '/admin/modules/server-chan'
},
{
path: 'modules/server-chan',
name: 'ServerChanSettings',
component: () => importWithRetry(() => import('@/views/admin/modules/ServerChanSettings.vue')),
meta: { module: 'server_chan_push' }
},
{
path: 'bark',
redirect: '/admin/modules/bark'
},
{
path: 'modules/bark',
name: 'BarkSettings',
component: () => importWithRetry(() => import('@/views/admin/modules/BarkSettings.vue')),
meta: { module: 'bark_push' }
},
{
path: 'email',
name: 'EmailSettings',
component: () => importWithRetry(() => import('@/views/admin/EmailSettings.vue'))
},
{
path: 'ldap',
name: 'LdapSettings',
component: () => importWithRetry(() => import('@/views/admin/LdapSettings.vue')),
meta: { module: 'ldap' }
},
{
path: 'oauth',
name: 'OAuthSettings',
component: () => importWithRetry(() => import('@/views/admin/OAuthSettings.vue')),
meta: { module: 'oauth' }
},
{
path: 'audit-logs',
name: 'AuditLogs',
component: () => importWithRetry(() => import('@/views/admin/AuditLogs.vue'))
},
{
path: 'cache-monitoring',
name: 'CacheMonitoring',
component: () => importWithRetry(() => import('@/views/admin/CacheMonitoring.vue'))
},
{
path: 'ip-security',
name: 'IPSecurity',
component: () => importWithRetry(() => import('@/views/admin/IPSecurity.vue'))
},
{
path: 'announcements',
name: 'AnnouncementManagement',
component: () => importWithRetry(() => import('@/views/user/Announcements.vue'))
},
{
path: 'async-tasks',
name: 'AsyncTasks',
component: () => importWithRetry(() => import('@/views/admin/AsyncTasks.vue'))
},
{
path: 'proxy-nodes',
name: 'ProxyNodes',
component: () => importWithRetry(() => import('@/views/admin/ProxyNodes.vue')),
meta: { module: 'proxy_nodes' }
},
{
path: 'gemini-files',
name: 'GeminiFilesManagement',
component: () => importWithRetry(() => import('@/views/admin/GeminiFilesManagement.vue'))
},
// 保留旧路由兼容性
{
path: 'video-tasks',
redirect: '/admin/async-tasks'
}
]
}
]
import { routes } from './routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
+228
View File
@@ -0,0 +1,228 @@
import type { RouteRecordRaw } from 'vue-router'
import { view } from './helpers'
export const adminRoutes: RouteRecordRaw[] = [
{
path: '/admin',
component: view(() => import('@/layouts/MainLayout.vue')),
meta: { requiresAuth: true, requiresAdmin: true },
children: [
{
path: 'dashboard',
name: 'AdminDashboard',
component: view(() => import('@/views/shared/Dashboard.vue'))
},
{
path: 'operations',
name: 'AdminOperationsDashboard',
component: view(() => import('@/views/admin/AdminOperationsDashboard.vue'))
},
{
path: 'users',
name: 'Users',
component: view(() => import('@/views/admin/Users.vue'))
},
{
path: 'keys',
name: 'ApiKeys',
component: view(() => import('@/views/admin/ApiKeys.vue'))
},
{
path: 'wallets',
name: 'WalletsManagement',
component: view(() => import('@/views/admin/WalletsManagement.vue'))
},
{
path: 'payment-gateways',
name: 'PaymentGatewaySettings',
component: view(() => import('@/views/admin/PaymentGatewaySettings.vue'))
},
{
path: 'billing-plans',
name: 'BillingPlansManagement',
component: view(() => import('@/views/admin/BillingPlansManagement.vue'))
},
{
path: 'referrals',
name: 'ReferralManagement',
component: view(() => import('@/views/admin/ReferralManagement.vue'))
},
{
path: 'management-tokens',
name: 'AdminManagementTokens',
component: view(() => import('@/views/user/ManagementTokens.vue')),
meta: { module: 'management_tokens' }
},
{
path: 'providers',
name: 'ProviderManagement',
component: view(() => import('@/views/admin/ProviderManagement.vue'))
},
{
path: 'pool',
name: 'PoolManagement',
component: view(() => import('@/views/admin/PoolManagement.vue'))
},
{
path: 'models',
name: 'ModelManagement',
component: view(() => import('@/views/admin/ModelManagement.vue'))
},
{
path: 'routing',
name: 'RoutingProfiles',
component: view(() => import('@/views/admin/RoutingProfiles.vue'))
},
{
path: 'routing/new',
name: 'RoutingProfileCreate',
component: view(() => import('@/views/admin/RoutingProfiles.vue'))
},
{
path: 'routing/:groupId',
name: 'RoutingProfileDetail',
component: view(() => import('@/views/admin/RoutingProfiles.vue'))
},
{
path: 'health-monitor',
name: 'HealthMonitor',
component: view(() => import('@/views/shared/HealthMonitor.vue'))
},
{
path: 'usage',
name: 'Usage',
component: view(() => import('@/views/shared/Usage.vue'))
},
{
path: 'user-stats',
name: 'UserStats',
component: view(() => import('@/views/admin/UserStats.vue'))
},
{
path: 'cost-analysis',
name: 'CostAnalysis',
component: view(() => import('@/views/admin/CostAnalysis.vue'))
},
{
path: 'performance-analysis',
name: 'PerformanceAnalysis',
component: view(() => import('@/views/admin/PerformanceAnalysis.vue'))
},
{
path: 'system',
name: 'SystemSettings',
component: view(() => import('@/views/admin/SystemSettings.vue'))
},
{
path: 'modules',
name: 'ModuleManagement',
component: view(() => import('@/views/admin/ModuleManagement.vue'))
},
{
path: 'model-directives',
name: 'ModelDirectivesManagement',
component: view(() => import('@/views/admin/ModelDirectivesManagement.vue')),
meta: { module: 'model_directives' }
},
{
path: 'modules/chat-pii-redaction',
name: 'ChatPiiRedactionModule',
component: view(() => import('@/views/admin/modules/ChatPiiRedaction.vue')),
meta: { module: 'chat_pii_redaction' }
},
{
path: 'modules/s3-backup',
name: 'S3BackupSettings',
component: view(() => import('@/views/admin/modules/S3BackupSettings.vue')),
meta: { module: 's3_backup' }
},
{
path: 'modules/important-notification',
redirect: '/admin/notification-service'
},
{
path: 'notification-service',
name: 'ImportantNotificationModule',
component: view(() => import('@/views/admin/modules/ImportantNotification.vue')),
meta: { module: 'important_notification' }
},
{
path: 'server-chan',
redirect: '/admin/modules/server-chan'
},
{
path: 'modules/server-chan',
name: 'ServerChanSettings',
component: view(() => import('@/views/admin/modules/ServerChanSettings.vue')),
meta: { module: 'server_chan_push' }
},
{
path: 'bark',
redirect: '/admin/modules/bark'
},
{
path: 'modules/bark',
name: 'BarkSettings',
component: view(() => import('@/views/admin/modules/BarkSettings.vue')),
meta: { module: 'bark_push' }
},
{
path: 'email',
name: 'EmailSettings',
component: view(() => import('@/views/admin/EmailSettings.vue'))
},
{
path: 'ldap',
name: 'LdapSettings',
component: view(() => import('@/views/admin/LdapSettings.vue')),
meta: { module: 'ldap' }
},
{
path: 'oauth',
name: 'OAuthSettings',
component: view(() => import('@/views/admin/OAuthSettings.vue')),
meta: { module: 'oauth' }
},
{
path: 'audit-logs',
name: 'AuditLogs',
component: view(() => import('@/views/admin/AuditLogs.vue'))
},
{
path: 'cache-monitoring',
name: 'CacheMonitoring',
component: view(() => import('@/views/admin/CacheMonitoring.vue'))
},
{
path: 'ip-security',
name: 'IPSecurity',
component: view(() => import('@/views/admin/IPSecurity.vue'))
},
{
path: 'announcements',
name: 'AnnouncementManagement',
component: view(() => import('@/views/user/Announcements.vue'))
},
{
path: 'async-tasks',
name: 'AsyncTasks',
component: view(() => import('@/views/admin/AsyncTasks.vue'))
},
{
path: 'proxy-nodes',
name: 'ProxyNodes',
component: view(() => import('@/views/admin/ProxyNodes.vue')),
meta: { module: 'proxy_nodes' }
},
{
path: 'gemini-files',
name: 'GeminiFilesManagement',
component: view(() => import('@/views/admin/GeminiFilesManagement.vue'))
},
{
path: 'video-tasks',
redirect: '/admin/async-tasks'
}
]
}
]
+73
View File
@@ -0,0 +1,73 @@
import type { RouteRecordRaw } from 'vue-router'
import { view } from './helpers'
export const dashboardRoutes: RouteRecordRaw[] = [
{
path: '/dashboard',
component: view(() => import('@/layouts/MainLayout.vue')),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'Dashboard',
component: view(() => import('@/views/shared/Dashboard.vue'))
},
{
path: 'api-keys',
name: 'MyApiKeys',
component: view(() => import('@/views/user/MyApiKeys.vue'))
},
{
path: 'management-tokens',
name: 'ManagementTokens',
component: view(() => import('@/views/user/ManagementTokens.vue')),
meta: { module: 'management_tokens' }
},
{
path: 'announcements',
name: 'Announcements',
component: view(() => import('@/views/user/Announcements.vue'))
},
{
path: 'usage',
name: 'MyUsage',
component: view(() => import('@/views/shared/Usage.vue'))
},
{
path: 'endpoint-status',
name: 'EndpointStatus',
component: view(() => import('@/views/shared/HealthMonitor.vue'))
},
{
path: 'settings',
name: 'Settings',
component: view(() => import('@/views/user/Settings.vue'))
},
{
path: 'wallet',
name: 'WalletCenter',
component: view(() => import('@/views/user/WalletCenter.vue'))
},
{
path: 'billing',
name: 'BillingPlans',
component: view(() => import('@/views/user/BillingPlans.vue'))
},
{
path: 'referral',
name: 'ReferralCenter',
component: view(() => import('@/views/user/ReferralCenter.vue'))
},
{
path: 'models',
name: 'ModelCatalog',
component: view(() => import('@/views/user/ModelCatalog.vue'))
},
{
path: 'async-tasks',
name: 'UserAsyncTasks',
component: view(() => import('@/views/admin/AsyncTasks.vue'))
}
]
}
]
+3
View File
@@ -0,0 +1,3 @@
import { importWithRetry } from '@/utils/importRetry'
export const view = <T>(loader: () => Promise<T>) => () => importWithRetry(loader)
+10
View File
@@ -0,0 +1,10 @@
import type { RouteRecordRaw } from 'vue-router'
import { adminRoutes } from './admin'
import { dashboardRoutes } from './dashboard'
import { publicRoutes } from './public'
export const routes: RouteRecordRaw[] = [
...publicRoutes,
...dashboardRoutes,
...adminRoutes,
]
+84
View File
@@ -0,0 +1,84 @@
import type { RouteRecordRaw } from 'vue-router'
import { view } from './helpers'
export const publicRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: view(() => import('@/views/public/Home.vue')),
meta: { requiresAuth: false }
},
{
path: '/register',
name: 'RegisterEntry',
component: view(() => import('@/views/public/Home.vue')),
meta: { requiresAuth: false }
},
{
path: '/privacy-policy',
name: 'PrivacyPolicy',
component: view(() => import('@/views/public/PrivacyPolicy.vue')),
meta: { requiresAuth: false }
},
{
path: '/guide',
component: view(() => import('@/views/public/guide/GuideLayout.vue')),
meta: { requiresAuth: false },
children: [
{
path: '',
name: 'GuideOverview',
component: view(() => import('@/views/public/guide/Overview.vue')),
meta: { requiresAuth: false }
},
{
path: 'architecture',
name: 'GuideArchitecture',
component: view(() => import('@/views/public/guide/ArchitectureGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'concepts',
name: 'GuideConcepts',
component: view(() => import('@/views/public/guide/ConceptsGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'strategy',
name: 'GuideStrategy',
component: view(() => import('@/views/public/guide/StrategyGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'advanced',
name: 'GuideAdvanced',
component: view(() => import('@/views/public/guide/AdvancedGuide.vue')),
meta: { requiresAuth: false }
},
{
path: 'faq',
name: 'GuideFaq',
component: view(() => import('@/views/public/guide/GuideFaq.vue')),
meta: { requiresAuth: false }
},
{
path: 'modules',
name: 'GuideModules',
component: view(() => import('@/views/public/guide/ModulesGuide.vue')),
meta: { requiresAuth: false }
}
]
},
{
path: '/logo-demo',
name: 'LogoColorDemo',
component: view(() => import('@/views/public/LogoColorDemo.vue')),
meta: { requiresAuth: false }
},
{
path: '/auth/callback',
name: 'AuthCallback',
component: view(() => import('@/views/public/AuthCallback.vue')),
meta: { requiresAuth: false }
}
]