diff --git a/app/components/auth/SubscriptionDialog.tsx b/app/components/auth/SubscriptionDialog.tsx index bbca148..510cf0e 100644 --- a/app/components/auth/SubscriptionDialog.tsx +++ b/app/components/auth/SubscriptionDialog.tsx @@ -3,21 +3,13 @@ import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '~/hooks/useAuth'; import { toast } from 'react-toastify'; import { PaymentModal } from './PaymentModal'; +import type { SubscriptionPlan } from '~/types/subscription'; interface SubscriptionDialogProps { isOpen: boolean; onClose: () => void; } -interface SubscriptionPlan { - _id: number; - name: string; - tokens: number; - price: number; - description: string; - save_percentage?: number; -} - interface UserSubscription { plan: SubscriptionPlan; tokensLeft: number; @@ -39,6 +31,12 @@ interface PaymentResponse { return_url: string; } +interface PurchaseResponse { + success: boolean; + paymentData?: PaymentResponse; + error?: string; +} + export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) { const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly'); const [subscriptionPlans, setSubscriptionPlans] = useState([]); @@ -48,31 +46,33 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) const [paymentData, setPaymentData] = useState(null); useEffect(() => { - if (isOpen && user) { + if (isOpen) { fetchSubscriptionData(); } - }, [isOpen, user]); + }, [isOpen]); const fetchSubscriptionData = async () => { setIsLoading(true); try { - const [plansResponse, userSubResponse] = await Promise.all([ - fetch('/api/subscription-plans'), - fetch('/api/user-subscription') - ]); - const plans = await plansResponse.json(); - const userSub = await userSubResponse.json(); - setSubscriptionPlans(plans); + const response = await fetch('/api/subscription-plans'); + if (!response.ok) { + throw new Error('获取订阅计划失败'); + } + const data = await response.json() as SubscriptionPlan[]; + setSubscriptionPlans(data); + + const userSubResponse = await fetch('/api/user-subscription'); + const userSub = await userSubResponse.json() as UserSubscription; setUserSubscription(userSub); } catch (error) { - console.error('Error fetching subscription data:', error); - toast.error('获取订阅信息失败,请稍后重试。'); + console.error('获取订阅数据时出错:', error); + toast.error('获取订阅信息失败,请稍后再试'); } finally { setIsLoading(false); } }; - const handlePurchase = async (planId: number) => { + const handlePurchase = async (planId: string) => { try { const response = await fetch('/api/purchase-subscription', { method: 'POST', @@ -84,11 +84,11 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) billingCycle, }), }); - const result = await response.json(); - if (response.ok && result.paymentData) { + const result = await response.json() as PurchaseResponse; + if (response.ok && result.success && result.paymentData) { setPaymentData(result.paymentData); } else { - toast.error(result.message || '获取支付信息失败,请稍后重试。'); + toast.error(result.error || '获取支付信息失败,请稍后重试。'); } } catch (error) { console.error('Error initiating purchase:', error); @@ -101,6 +101,19 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) toast.success('订阅成功!'); }, [fetchSubscriptionData]); + // 类型守卫函数 + function isSubscriptionPlan(plan: any): plan is SubscriptionPlan { + return ( + typeof plan === 'object' && + typeof plan._id === 'string' && + typeof plan.name === 'string' && + typeof plan.tokens === 'number' && + typeof plan.price === 'number' && + typeof plan.description === 'string' && + (plan.save_percentage === null || typeof plan.save_percentage === 'number') + ); + } + if (!user || isLoading) return null; return ( diff --git a/app/middleware/auth.server.ts b/app/middleware/auth.server.ts new file mode 100644 index 0000000..6e1670f --- /dev/null +++ b/app/middleware/auth.server.ts @@ -0,0 +1,17 @@ +import { json } from '@remix-run/cloudflare'; +import { verifyToken } from '~/utils/auth.server'; + +export async function requireAuth(request: Request) { + const authHeader = request.headers.get('Authorization'); + if (!authHeader) { + throw json({ error: '缺少授权头' }, { status: 401 }); + } + + const token = authHeader.split(' ')[1]; + const payload = verifyToken(token); + if (!payload) { + throw json({ error: '无效的令牌' }, { status: 401 }); + } + + return payload.userId; +} diff --git a/app/routes/api.purchase-subscription.ts b/app/routes/api.purchase-subscription.ts index 2c0b376..24b7a77 100644 --- a/app/routes/api.purchase-subscription.ts +++ b/app/routes/api.purchase-subscription.ts @@ -1,17 +1,23 @@ import { json } from '@remix-run/cloudflare'; import { db } from '~/utils/db.server'; -import { requireUserId } from '../utils/session.server'; // 使用相对路径 import SDPay from '~/utils/SDPay.server'; +import { requireAuth } from '~/middleware/auth.server'; export async function action({ request }: { request: Request }) { - const userId = await requireUserId(request); + let userId; + try { + userId = await requireAuth(request); + } catch (error) { + return error as Response; + } + const { planId, billingCycle } = await request.json() as { planId: string; billingCycle: string }; try { // 获取订阅计划详情 const plan = await db('subscription_plans').where('_id', planId).first(); if (!plan) { - return json({ error: 'Invalid subscription plan' }, { status: 400 }); + return json({ error: '无效的订阅计划' }, { status: 400 }); } // 计算实际价格和代币数量 @@ -47,7 +53,7 @@ export async function action({ request }: { request: Request }) { return json({ success: true, paymentData }); } catch (error) { - console.error('Error initiating subscription purchase:', error); - return json({ error: 'Failed to initiate subscription purchase' }, { status: 500 }); + console.error('初始化订阅购买时出错:', error); + return json({ error: '初始化订阅购买失败' }, { status: 500 }); } } diff --git a/app/routes/api/auth/login.ts b/app/routes/api/auth/login.ts deleted file mode 100644 index 0519ecb..0000000 --- a/app/routes/api/auth/login.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/routes/api/auth/register.ts b/app/routes/api/auth/register.ts deleted file mode 100644 index 0519ecb..0000000 --- a/app/routes/api/auth/register.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/routes/login.tsx b/app/routes/login.tsx deleted file mode 100644 index cb09d42..0000000 --- a/app/routes/login.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Login } from '~/components/auth/Login'; - -export default function LoginPage() { - return ; -} diff --git a/app/routes/register.tsx b/app/routes/register.tsx deleted file mode 100644 index 16b108b..0000000 --- a/app/routes/register.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Register } from '~/components/auth/Register'; - -export default function RegisterPage() { - return ; -} diff --git a/app/types/subscription.ts b/app/types/subscription.ts new file mode 100644 index 0000000..10db805 --- /dev/null +++ b/app/types/subscription.ts @@ -0,0 +1,8 @@ +export interface SubscriptionPlan { + _id: string; + name: string; + tokens: number; + price: number; + description: string; + save_percentage: number | null; +} diff --git a/app/utils/session.server.ts b/app/utils/session.server.ts deleted file mode 100644 index 0519ecb..0000000 --- a/app/utils/session.server.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file