feat: 完善认证系统
- 修复 Storage 层使用 AsyncStorage - 统一 Token 存储键名为 bestaibest.better-auth.session_token - 启用 401 自动跳转登录页并显示 Toast 提示 - 添加全局认证守卫(AuthGuard 组件) - 修复 x-ownerid header 配置(从环境变量读取商户ID) - 导出 loomart API 供活动数据使用 主要修改: - lib/storage.native.ts: 新建,使用 AsyncStorage - lib/storage.ts: 添加错误处理和注释 - lib/fetch-logger.ts: 统一 Token 键,启用 401 拦截 - lib/auth.ts: 导出 TOKEN_KEY,修复 x-ownerid,导出 loomart - app/_layout.tsx: 添加 AuthGuard 组件实现路由守卫 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
105
app/_layout.tsx
105
app/_layout.tsx
@@ -1,43 +1,110 @@
|
||||
import '../global.css'
|
||||
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
|
||||
import { Stack } from 'expo-router'
|
||||
import { Stack, usePathname, useRouter } from 'expo-router'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { useEffect } from 'react'
|
||||
import 'react-native-reanimated'
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||
import { View, ActivityIndicator } from 'react-native'
|
||||
|
||||
import { useColorScheme } from '@/hooks/use-color-scheme'
|
||||
import { KeyboardProvider } from 'react-native-keyboard-controller'
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||
import { ModalPortal } from '@/components/ui'
|
||||
import '@/lib/i18n'
|
||||
import { useSession } from '@/lib/auth'
|
||||
|
||||
export const unstable_settings = {
|
||||
anchor: '(tabs)',
|
||||
}
|
||||
|
||||
// 受保护的路由(需要登录才能访问)
|
||||
const PROTECTED_ROUTES = [
|
||||
'/(tabs)',
|
||||
'/channels',
|
||||
'/generateVideo',
|
||||
'/generationRecord',
|
||||
'/worksList',
|
||||
'/membership',
|
||||
'/changePassword',
|
||||
]
|
||||
|
||||
// 公开路由(无需登录)
|
||||
const PUBLIC_ROUTES = [
|
||||
'/auth',
|
||||
'/modal',
|
||||
'/searchTemplate',
|
||||
'/searchResults',
|
||||
'/searchWorks',
|
||||
'/searchWorksResults',
|
||||
'/templateDetail',
|
||||
'/terms',
|
||||
'/privacy',
|
||||
]
|
||||
|
||||
// 认证守卫组件
|
||||
function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const { data: session, isPending } = useSession()
|
||||
|
||||
useEffect(() => {
|
||||
// 加载中不处理
|
||||
if (isPending) return
|
||||
|
||||
// 检查是否在受保护路由
|
||||
const isProtectedRoute = PROTECTED_ROUTES.some(route => pathname?.startsWith(route))
|
||||
|
||||
// 未登录访问受保护路由 -> 跳转到登录页
|
||||
if (isProtectedRoute && !session?.user) {
|
||||
router.replace('/auth')
|
||||
return
|
||||
}
|
||||
|
||||
// 已登录访问登录页 -> 跳转到首页
|
||||
if (pathname === '/auth' && session?.user) {
|
||||
router.replace('/(tabs)')
|
||||
return
|
||||
}
|
||||
}, [pathname, session, isPending, router])
|
||||
|
||||
// 加载中显示 loading
|
||||
if (isPending) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#090A0B' }}>
|
||||
<ActivityIndicator size="large" color="#9966FF" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
// 路由层
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<Providers>
|
||||
<Stack>
|
||||
<Stack.Screen name="auth" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="channels" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="searchTemplate" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="searchResults" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="generationRecord" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="generateVideo" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="templateDetail" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="membership" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="terms" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="privacy" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="worksList" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="searchWorks" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="searchWorksResults" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="changePassword" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
<AuthGuard>
|
||||
<Stack>
|
||||
<Stack.Screen name="auth" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="channels" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="searchTemplate" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="searchResults" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="generationRecord" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="generateVideo" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="templateDetail" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="membership" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="terms" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="privacy" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="worksList" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="searchWorks" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="searchWorksResults" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="changePassword" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</AuthGuard>
|
||||
<StatusBar style="auto" />
|
||||
</Providers>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user