@cogito.ai/cli 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -22
- package/dist/index.js +9 -15
- package/dist/templates/web-nextjs/.github/copilot-instructions.md +5 -6
- package/dist/templates/web-nextjs/README.md +25 -24
- package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +1 -1
- package/dist/templates/web-nextjs/apps/docs/.source/server.ts +6 -6
- package/dist/templates/web-nextjs/apps/docs/app/docs/[[...slug]]/page.tsx +1 -6
- package/dist/templates/web-nextjs/apps/docs/app/docs/layout.tsx +1 -4
- package/dist/templates/web-nextjs/apps/docs/app/llms-full.txt/route.ts +3 -1
- package/dist/templates/web-nextjs/apps/docs/next-env.d.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/.env.example +35 -0
- package/dist/templates/web-nextjs/apps/web/.github/copilot-instructions.md +53 -6
- package/dist/templates/web-nextjs/apps/web/.github/skills/impeccable/SKILL.md +55 -0
- package/dist/templates/web-nextjs/apps/web/DESIGN.md +65 -0
- package/dist/templates/web-nextjs/apps/web/messages/en.json +151 -5
- package/dist/templates/web-nextjs/apps/web/messages/zh.json +151 -5
- package/dist/templates/web-nextjs/apps/web/next-env.d.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/next.config.ts +3 -3
- package/dist/templates/web-nextjs/apps/web/package.json +4 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/forgot-password/page.tsx +167 -38
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/login/page.tsx +13 -3
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +4 -1
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/signup/page.tsx +18 -17
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/dashboard/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/[paymentId]/page.tsx +88 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/checkout/page.tsx +170 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/pricing/page.tsx +120 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/layout.tsx +45 -2
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/profile/page.tsx +2 -8
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/subscription/page.tsx +128 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/about/page.tsx +3 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/globals.css +17 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/help/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/layout.tsx +10 -8
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/page.tsx +22 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/privacy/page.tsx +3 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/terms/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/create/route.ts +43 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/notify/route.ts +105 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/query/route.ts +54 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/return/route.ts +20 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/create/route.ts +52 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/create/route.ts +58 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/notify/route.ts +92 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/query/route.ts +54 -0
- package/dist/templates/web-nextjs/apps/web/src/app/auth/callback/route.ts +2 -3
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/app-sidebar.tsx +13 -16
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/chart-area-interactive.tsx +122 -146
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/data-table.tsx +84 -149
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-documents.tsx +7 -16
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-main.tsx +4 -4
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-secondary.tsx +4 -4
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-user.tsx +12 -21
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/page.tsx +10 -13
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/section-cards.tsx +5 -9
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/site-header.tsx +6 -7
- package/dist/templates/web-nextjs/apps/web/src/components/landing/features.tsx +63 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/footer.tsx +48 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/header.tsx +97 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/hero.tsx +45 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/how-it-works.tsx +35 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/pricing-teaser.tsx +23 -0
- package/dist/templates/web-nextjs/apps/web/src/components/profile/profile-form.tsx +6 -4
- package/dist/templates/web-nextjs/apps/web/src/components/providers/theme-provider.tsx +16 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/alert-dialog.tsx +32 -49
- package/dist/templates/web-nextjs/apps/web/src/components/ui/alert.tsx +16 -23
- package/dist/templates/web-nextjs/apps/web/src/components/ui/avatar.tsx +25 -38
- package/dist/templates/web-nextjs/apps/web/src/components/ui/badge.tsx +16 -18
- package/dist/templates/web-nextjs/apps/web/src/components/ui/breadcrumb.tsx +19 -26
- package/dist/templates/web-nextjs/apps/web/src/components/ui/button.tsx +23 -24
- package/dist/templates/web-nextjs/apps/web/src/components/ui/card.tsx +19 -36
- package/dist/templates/web-nextjs/apps/web/src/components/ui/chart.tsx +60 -94
- package/dist/templates/web-nextjs/apps/web/src/components/ui/checkbox.tsx +8 -11
- package/dist/templates/web-nextjs/apps/web/src/components/ui/collapsible.tsx +5 -17
- package/dist/templates/web-nextjs/apps/web/src/components/ui/command.tsx +25 -48
- package/dist/templates/web-nextjs/apps/web/src/components/ui/dialog.tsx +21 -35
- package/dist/templates/web-nextjs/apps/web/src/components/ui/drawer.tsx +24 -35
- package/dist/templates/web-nextjs/apps/web/src/components/ui/dropdown-menu.tsx +26 -55
- package/dist/templates/web-nextjs/apps/web/src/components/ui/field.tsx +62 -76
- package/dist/templates/web-nextjs/apps/web/src/components/ui/form.tsx +19 -34
- package/dist/templates/web-nextjs/apps/web/src/components/ui/input-otp.tsx +13 -20
- package/dist/templates/web-nextjs/apps/web/src/components/ui/input.tsx +6 -6
- package/dist/templates/web-nextjs/apps/web/src/components/ui/label.tsx +6 -6
- package/dist/templates/web-nextjs/apps/web/src/components/ui/pagination.tsx +21 -42
- package/dist/templates/web-nextjs/apps/web/src/components/ui/popover.tsx +16 -31
- package/dist/templates/web-nextjs/apps/web/src/components/ui/progress.tsx +5 -8
- package/dist/templates/web-nextjs/apps/web/src/components/ui/radio-group.tsx +8 -8
- package/dist/templates/web-nextjs/apps/web/src/components/ui/scroll-area.tsx +10 -12
- package/dist/templates/web-nextjs/apps/web/src/components/ui/select.tsx +26 -41
- package/dist/templates/web-nextjs/apps/web/src/components/ui/separator.tsx +7 -7
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sheet.tsx +29 -38
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sidebar.tsx +157 -189
- package/dist/templates/web-nextjs/apps/web/src/components/ui/skeleton.tsx +3 -3
- package/dist/templates/web-nextjs/apps/web/src/components/ui/slider.tsx +10 -15
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sonner.tsx +13 -7
- package/dist/templates/web-nextjs/apps/web/src/components/ui/switch.tsx +9 -9
- package/dist/templates/web-nextjs/apps/web/src/components/ui/table.tsx +24 -48
- package/dist/templates/web-nextjs/apps/web/src/components/ui/tabs.tsx +21 -31
- package/dist/templates/web-nextjs/apps/web/src/components/ui/textarea.tsx +5 -5
- package/dist/templates/web-nextjs/apps/web/src/components/ui/theme-toggle.tsx +23 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/toggle-group.tsx +15 -16
- package/dist/templates/web-nextjs/apps/web/src/components/ui/toggle.tsx +14 -15
- package/dist/templates/web-nextjs/apps/web/src/components/ui/tooltip.tsx +8 -12
- package/dist/templates/web-nextjs/apps/web/src/core/repositories/IAuthRepository.ts +2 -0
- package/dist/templates/web-nextjs/apps/web/src/core/types/auth.ts +1 -3
- package/dist/templates/web-nextjs/apps/web/src/features/auth/actions.ts +57 -1
- package/dist/templates/web-nextjs/apps/web/src/features/auth/index.ts +2 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/client.ts +36 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/server.ts +83 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/index.ts +4 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-badge.tsx +9 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-feature-comparison.tsx +70 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/quota-warning-banner.tsx +37 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/upgrade-button.tsx +42 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/hooks.ts +141 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-helpers.ts +27 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-service.ts +56 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/service.ts +55 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/types.ts +73 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/client.ts +33 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/server.ts +147 -0
- package/dist/templates/web-nextjs/apps/web/src/hooks/use-mobile.ts +4 -4
- package/dist/templates/web-nextjs/apps/web/src/i18n/config.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/src/infra/db/SupabaseAuthRepository.ts +48 -4
- package/dist/templates/web-nextjs/apps/web/src/infra/db/client.ts +1 -4
- package/dist/templates/web-nextjs/apps/web/src/lib/supabase/admin.ts +23 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/utils.ts +2 -2
- package/dist/templates/web-nextjs/apps/web/src/lib/validations/auth.ts +13 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/client.ts +66 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/crypto.ts +99 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/types.ts +10 -0
- package/dist/templates/web-nextjs/apps/web/src/styles/tokens.css +58 -0
- package/dist/templates/web-nextjs/pnpm-lock.yaml +319 -0
- package/dist/templates/web-nextjs/supabase/migrations/20250608_add_subscription_tables.sql +125 -0
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type PlanCode = 'free' | 'pro' | 'enterprise'
|
|
2
|
+
|
|
3
|
+
export type BillingCycle = 'monthly' | 'yearly'
|
|
4
|
+
|
|
5
|
+
export type PaymentMethod = 'alipay' | 'wechat' | 'stripe'
|
|
6
|
+
|
|
7
|
+
export type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded' | 'cancelled'
|
|
8
|
+
|
|
9
|
+
export type SubscriptionStatus = 'trial' | 'active' | 'expired' | 'cancelled' | 'pending'
|
|
10
|
+
|
|
11
|
+
export type PaymentChannel = 'alipay_page' | 'alipay_wap' | 'wechat_native' | 'wechat_h5'
|
|
12
|
+
|
|
13
|
+
export interface SubscriptionPlan {
|
|
14
|
+
id: number
|
|
15
|
+
plan_code: PlanCode
|
|
16
|
+
plan_name: string
|
|
17
|
+
description: string | null
|
|
18
|
+
price_monthly: number
|
|
19
|
+
price_yearly: number
|
|
20
|
+
features: Record<string, unknown>
|
|
21
|
+
display_order: number
|
|
22
|
+
is_active: boolean
|
|
23
|
+
is_featured: boolean
|
|
24
|
+
created_at: string
|
|
25
|
+
updated_at: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UserSubscription {
|
|
29
|
+
id: number
|
|
30
|
+
user_id: string
|
|
31
|
+
plan_id: number
|
|
32
|
+
status: SubscriptionStatus
|
|
33
|
+
billing_cycle: BillingCycle
|
|
34
|
+
current_period_start: string | null
|
|
35
|
+
current_period_end: string | null
|
|
36
|
+
cancelled_at: string | null
|
|
37
|
+
cancel_at_period_end: boolean
|
|
38
|
+
last_payment_id: number | null
|
|
39
|
+
next_billing_date: string | null
|
|
40
|
+
metadata: Record<string, unknown> | null
|
|
41
|
+
created_at: string
|
|
42
|
+
updated_at: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Payment {
|
|
46
|
+
id: number
|
|
47
|
+
user_id: string
|
|
48
|
+
subscription_id: number | null
|
|
49
|
+
order_no: string
|
|
50
|
+
amount: number
|
|
51
|
+
currency: string
|
|
52
|
+
payment_method: PaymentMethod
|
|
53
|
+
payment_channel: PaymentChannel | null
|
|
54
|
+
status: PaymentStatus
|
|
55
|
+
provider_trade_no: string | null
|
|
56
|
+
provider_response: Record<string, unknown> | null
|
|
57
|
+
paid_at: string | null
|
|
58
|
+
refunded_at: string | null
|
|
59
|
+
metadata: Record<string, unknown> | null
|
|
60
|
+
created_at: string
|
|
61
|
+
updated_at: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CreatePaymentParams {
|
|
65
|
+
user_id: string
|
|
66
|
+
subscription_id?: number
|
|
67
|
+
order_no: string
|
|
68
|
+
amount: number
|
|
69
|
+
currency?: string
|
|
70
|
+
payment_method: PaymentMethod
|
|
71
|
+
payment_channel?: PaymentChannel
|
|
72
|
+
metadata?: Record<string, unknown>
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
interface WechatPayResult {
|
|
4
|
+
type: 'qrcode' | 'redirect'
|
|
5
|
+
codeUrl?: string
|
|
6
|
+
h5Url?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function initiateWechatPayment(
|
|
10
|
+
paymentId: number,
|
|
11
|
+
userId: string,
|
|
12
|
+
): Promise<WechatPayResult> {
|
|
13
|
+
const res = await fetch('/api/payments/wechat/create', {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
body: JSON.stringify({ paymentId, userId }),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error('Failed to initiate WeChat payment')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const data = (await res.json()) as WechatPayResult
|
|
24
|
+
|
|
25
|
+
if (data.type === 'qrcode' && data.codeUrl) {
|
|
26
|
+
return { type: 'qrcode', codeUrl: data.codeUrl }
|
|
27
|
+
} else if (data.type === 'redirect' && data.h5Url) {
|
|
28
|
+
window.location.href = data.h5Url
|
|
29
|
+
return { type: 'redirect', h5Url: data.h5Url }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error('Unexpected WeChat payment response')
|
|
33
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { wxpayRequest } from '@/lib/wechat-pay/client'
|
|
2
|
+
import type { WechatPayConfig } from '@/lib/wechat-pay/types'
|
|
3
|
+
import { decryptResource, verifySignature } from '@/lib/wechat-pay/crypto'
|
|
4
|
+
|
|
5
|
+
export function getWechatPayConfig(): WechatPayConfig {
|
|
6
|
+
const appId = process.env.WECHAT_PAY_APP_ID
|
|
7
|
+
const mchId = process.env.WECHAT_PAY_MCH_ID
|
|
8
|
+
const privateKey = process.env.WECHAT_PAY_PRIVATE_KEY
|
|
9
|
+
const serialNo = process.env.WECHAT_PAY_SERIAL_NO
|
|
10
|
+
const apiV3Key = process.env.WECHAT_PAY_API_V3_KEY
|
|
11
|
+
const platformPublicKey = process.env.WECHAT_PAY_PLATFORM_PUBLIC_KEY
|
|
12
|
+
const baseUrl = process.env.WECHAT_PAY_BASE_URL || 'https://api.mch.weixin.qq.com'
|
|
13
|
+
const notifyUrl = process.env.WECHAT_PAY_NOTIFY_URL
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
!appId ||
|
|
17
|
+
!mchId ||
|
|
18
|
+
!privateKey ||
|
|
19
|
+
!serialNo ||
|
|
20
|
+
!apiV3Key ||
|
|
21
|
+
!platformPublicKey ||
|
|
22
|
+
!notifyUrl
|
|
23
|
+
) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'Missing WeChat Pay credentials. Ensure all WECHAT_PAY_* environment variables are set.',
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
appId,
|
|
31
|
+
mchId,
|
|
32
|
+
privateKey,
|
|
33
|
+
serialNo,
|
|
34
|
+
apiV3Key,
|
|
35
|
+
platformPublicKey,
|
|
36
|
+
baseUrl,
|
|
37
|
+
notifyUrl,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface CreateWechatPayParams {
|
|
42
|
+
description: string
|
|
43
|
+
outTradeNo: string
|
|
44
|
+
notifyUrl: string
|
|
45
|
+
amount: { total: number; currency?: string }
|
|
46
|
+
sceneInfo?: object
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function createWechatNativePay(params: CreateWechatPayParams) {
|
|
50
|
+
const config = getWechatPayConfig()
|
|
51
|
+
const body = {
|
|
52
|
+
appid: config.appId,
|
|
53
|
+
mchid: config.mchId,
|
|
54
|
+
description: params.description,
|
|
55
|
+
out_trade_no: params.outTradeNo,
|
|
56
|
+
notify_url: params.notifyUrl,
|
|
57
|
+
amount: params.amount,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return wxpayRequest<{ code_url: string }>({
|
|
61
|
+
config,
|
|
62
|
+
method: 'post',
|
|
63
|
+
url: '/v3/pay/transactions/native',
|
|
64
|
+
body,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function createWechatH5Pay(params: CreateWechatPayParams) {
|
|
69
|
+
const config = getWechatPayConfig()
|
|
70
|
+
const body = {
|
|
71
|
+
appid: config.appId,
|
|
72
|
+
mchid: config.mchId,
|
|
73
|
+
description: params.description,
|
|
74
|
+
out_trade_no: params.outTradeNo,
|
|
75
|
+
notify_url: params.notifyUrl,
|
|
76
|
+
amount: params.amount,
|
|
77
|
+
scene_info: params.sceneInfo,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return wxpayRequest<{ h5_url: string }>({
|
|
81
|
+
config,
|
|
82
|
+
method: 'post',
|
|
83
|
+
url: '/v3/pay/transactions/h5',
|
|
84
|
+
body,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function queryWechatOrder(outTradeNo: string) {
|
|
89
|
+
const config = getWechatPayConfig()
|
|
90
|
+
return wxpayRequest<{ trade_state: string }>({
|
|
91
|
+
config,
|
|
92
|
+
method: 'get',
|
|
93
|
+
url: `/v3/pay/transactions/out-trade-no/${outTradeNo}`,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface VerifyAndDecryptNotifyResult {
|
|
98
|
+
outTradeNo: string
|
|
99
|
+
tradeState: string
|
|
100
|
+
transactionId: string
|
|
101
|
+
amount: number
|
|
102
|
+
currency: string
|
|
103
|
+
appId: string
|
|
104
|
+
mchId: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function verifyAndDecryptNotify(
|
|
108
|
+
headers: Record<string, string | undefined>,
|
|
109
|
+
rawBody: string,
|
|
110
|
+
): Promise<VerifyAndDecryptNotifyResult> {
|
|
111
|
+
const config = getWechatPayConfig()
|
|
112
|
+
const timestamp = headers['wechatpay-timestamp'] || headers['Wechatpay-Timestamp'] || ''
|
|
113
|
+
const nonce = headers['wechatpay-nonce'] || headers['Wechatpay-Nonce'] || ''
|
|
114
|
+
const signature = headers['wechatpay-signature'] || headers['Wechatpay-Signature'] || ''
|
|
115
|
+
|
|
116
|
+
const isValid = verifySignature({
|
|
117
|
+
timestamp,
|
|
118
|
+
nonce,
|
|
119
|
+
body: rawBody,
|
|
120
|
+
signature,
|
|
121
|
+
platformPublicKey: config.platformPublicKey,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (!isValid) {
|
|
125
|
+
throw new Error('Invalid WeChat Pay signature')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const bodyData = JSON.parse(rawBody)
|
|
129
|
+
const resource = bodyData.resource
|
|
130
|
+
const decrypted = decryptResource({
|
|
131
|
+
ciphertext: resource.ciphertext,
|
|
132
|
+
associatedData: resource.associated_data,
|
|
133
|
+
nonce: resource.nonce,
|
|
134
|
+
apiV3Key: config.apiV3Key,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const transaction = JSON.parse(decrypted)
|
|
138
|
+
return {
|
|
139
|
+
outTradeNo: transaction.out_trade_no,
|
|
140
|
+
tradeState: transaction.trade_state,
|
|
141
|
+
transactionId: transaction.transaction_id,
|
|
142
|
+
amount: transaction.amount?.total,
|
|
143
|
+
currency: transaction.amount?.currency,
|
|
144
|
+
appId: transaction.appid,
|
|
145
|
+
mchId: transaction.mchid,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import * as React from
|
|
3
|
+
import * as React from 'react'
|
|
4
4
|
|
|
5
5
|
const MOBILE_BREAKPOINT = 768
|
|
6
6
|
|
|
@@ -12,9 +12,9 @@ export function useIsMobile() {
|
|
|
12
12
|
const onChange = () => {
|
|
13
13
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
14
14
|
}
|
|
15
|
-
mql.addEventListener(
|
|
15
|
+
mql.addEventListener('change', onChange)
|
|
16
16
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
17
|
-
return () => mql.removeEventListener(
|
|
17
|
+
return () => mql.removeEventListener('change', onChange)
|
|
18
18
|
}, [])
|
|
19
19
|
|
|
20
20
|
return !!isMobile
|
|
@@ -20,9 +20,7 @@ export class SupabaseAuthRepository implements IAuthRepository {
|
|
|
20
20
|
const supabase = await getServerClient()
|
|
21
21
|
const { data, error } = await supabase.auth.signUp({ email, password })
|
|
22
22
|
if (error) {
|
|
23
|
-
const msg = error.message.toLowerCase().includes('already')
|
|
24
|
-
? '该邮箱已被注册'
|
|
25
|
-
: error.message
|
|
23
|
+
const msg = error.message.toLowerCase().includes('already') ? '该邮箱已被注册' : error.message
|
|
26
24
|
return { user: null, error: msg }
|
|
27
25
|
}
|
|
28
26
|
const u = data.user
|
|
@@ -39,7 +37,9 @@ export class SupabaseAuthRepository implements IAuthRepository {
|
|
|
39
37
|
|
|
40
38
|
async getSession(): Promise<AuthUser | null> {
|
|
41
39
|
const supabase = await getServerClient()
|
|
42
|
-
const {
|
|
40
|
+
const {
|
|
41
|
+
data: { user },
|
|
42
|
+
} = await supabase.auth.getUser()
|
|
43
43
|
if (!user) return null
|
|
44
44
|
return { id: user.id, email: user.email ?? '', createdAt: user.created_at }
|
|
45
45
|
}
|
|
@@ -85,4 +85,48 @@ export class SupabaseAuthRepository implements IAuthRepository {
|
|
|
85
85
|
}
|
|
86
86
|
return { user: null, error: null }
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
async sendPasswordResetOTP(email: string): Promise<AuthResult> {
|
|
90
|
+
const supabase = await getServerClient()
|
|
91
|
+
const { error } = await supabase.auth.signInWithOtp({
|
|
92
|
+
email,
|
|
93
|
+
options: {
|
|
94
|
+
shouldCreateUser: false, // Don't create user if not exists
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
if (error) {
|
|
98
|
+
return { user: null, error: error.message }
|
|
99
|
+
}
|
|
100
|
+
return { user: null, error: null }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async verifyPasswordResetOTP(
|
|
104
|
+
email: string,
|
|
105
|
+
token: string,
|
|
106
|
+
newPassword: string,
|
|
107
|
+
): Promise<AuthResult> {
|
|
108
|
+
const supabase = await getServerClient()
|
|
109
|
+
const { data, error } = await supabase.auth.verifyOtp({
|
|
110
|
+
email,
|
|
111
|
+
token,
|
|
112
|
+
type: 'email',
|
|
113
|
+
})
|
|
114
|
+
if (error) {
|
|
115
|
+
return { user: null, error: error.message }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// After verifying OTP, update the password
|
|
119
|
+
const { error: updateError } = await supabase.auth.updateUser({
|
|
120
|
+
password: newPassword,
|
|
121
|
+
})
|
|
122
|
+
if (updateError) {
|
|
123
|
+
return { user: null, error: updateError.message }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const u = data.user
|
|
127
|
+
return {
|
|
128
|
+
user: u ? { id: u.id, email: u.email ?? '', createdAt: u.created_at } : null,
|
|
129
|
+
error: null,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
88
132
|
}
|
|
@@ -66,10 +66,7 @@ export function getBrowserClient() {
|
|
|
66
66
|
* await supabase.auth.getUser() // refreshes session token
|
|
67
67
|
* ```
|
|
68
68
|
*/
|
|
69
|
-
export function createMiddlewareClient(
|
|
70
|
-
request: NextRequest,
|
|
71
|
-
response: NextResponse,
|
|
72
|
-
) {
|
|
69
|
+
export function createMiddlewareClient(request: NextRequest, response: NextResponse) {
|
|
73
70
|
const supabase = createServerClient(
|
|
74
71
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
75
72
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Supabase admin client using the service role key.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ WARNING: This client bypasses Row Level Security (RLS).
|
|
7
|
+
* Use ONLY in server-side contexts (Route Handlers, Server Actions).
|
|
8
|
+
* NEVER expose this client to the browser.
|
|
9
|
+
*/
|
|
10
|
+
export function createAdminClient() {
|
|
11
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
12
|
+
const key = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
13
|
+
|
|
14
|
+
if (!url || !key) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Missing Supabase admin credentials. Ensure NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set.',
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return createClient(url, key, {
|
|
21
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -20,6 +20,18 @@ export const forgotPasswordSchema = z.object({
|
|
|
20
20
|
email: z.string().email('请输入有效邮箱'),
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
+
export const resetPasswordWithOTPSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
email: z.string().email('请输入有效邮箱'),
|
|
26
|
+
token: z.string().min(6, '验证码至少 6 位'),
|
|
27
|
+
password: z.string().min(8, '密码至少 8 位'),
|
|
28
|
+
confirmPassword: z.string(),
|
|
29
|
+
})
|
|
30
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
31
|
+
message: '两次密码不一致',
|
|
32
|
+
path: ['confirmPassword'],
|
|
33
|
+
})
|
|
34
|
+
|
|
23
35
|
export const resetPasswordSchema = z
|
|
24
36
|
.object({
|
|
25
37
|
password: z.string().min(8, '密码至少 8 位'),
|
|
@@ -37,5 +49,6 @@ export const displayNameSchema = z.object({
|
|
|
37
49
|
export type SignInInput = z.infer<typeof signInSchema>
|
|
38
50
|
export type SignUpInput = z.infer<typeof signUpSchema>
|
|
39
51
|
export type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>
|
|
52
|
+
export type ResetPasswordWithOTPInput = z.infer<typeof resetPasswordWithOTPSchema>
|
|
40
53
|
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>
|
|
41
54
|
export type DisplayNameInput = z.infer<typeof displayNameSchema>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { buildAuthorizationHeader, generateNonce, signRequest } from './crypto'
|
|
2
|
+
import type { WechatPayConfig } from './types'
|
|
3
|
+
|
|
4
|
+
interface WxpayRequestParams<T = unknown> {
|
|
5
|
+
config: WechatPayConfig
|
|
6
|
+
method: string
|
|
7
|
+
url: string
|
|
8
|
+
body?: object
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function wxpayRequest<T>(params: WxpayRequestParams): Promise<T> {
|
|
12
|
+
const { config, method, url, body } = params
|
|
13
|
+
const timestamp = Math.floor(Date.now() / 1000).toString()
|
|
14
|
+
const nonce = generateNonce()
|
|
15
|
+
const bodyStr = body ? JSON.stringify(body) : ''
|
|
16
|
+
|
|
17
|
+
const signature = signRequest({
|
|
18
|
+
method,
|
|
19
|
+
url,
|
|
20
|
+
timestamp,
|
|
21
|
+
nonce,
|
|
22
|
+
body: bodyStr,
|
|
23
|
+
privateKey: config.privateKey,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const authorization = buildAuthorizationHeader({
|
|
27
|
+
mchId: config.mchId,
|
|
28
|
+
serialNo: config.serialNo,
|
|
29
|
+
timestamp,
|
|
30
|
+
nonce,
|
|
31
|
+
signature,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const headers: Record<string, string> = {
|
|
35
|
+
Accept: 'application/json',
|
|
36
|
+
Authorization: authorization,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
'User-Agent': 'Mozilla/5.0 (compatible; WebNextjs/1.0)',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const res = await fetch(`${config.baseUrl}${url}`, {
|
|
42
|
+
method: method.toUpperCase(),
|
|
43
|
+
headers,
|
|
44
|
+
body: bodyStr || null,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const data = (await res.json()) as T
|
|
48
|
+
|
|
49
|
+
// Check for WeChat Pay API errors
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const errorData = data as Record<string, unknown>
|
|
52
|
+
const errorMessage =
|
|
53
|
+
typeof errorData.message === 'string'
|
|
54
|
+
? errorData.message
|
|
55
|
+
: `WeChat Pay API error: ${res.status}`
|
|
56
|
+
const error = new Error(errorMessage)
|
|
57
|
+
;(error as Error & { code?: string; status?: number; raw?: unknown }).code = String(
|
|
58
|
+
errorData.code || '',
|
|
59
|
+
)
|
|
60
|
+
;(error as Error & { code?: string; status?: number; raw?: unknown }).status = res.status
|
|
61
|
+
;(error as Error & { code?: string; status?: number; raw?: unknown }).raw = errorData
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return data
|
|
66
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a random nonce string (16 hex chars, uppercase)
|
|
5
|
+
*/
|
|
6
|
+
export function generateNonce(): string {
|
|
7
|
+
return crypto.randomBytes(16).toString('hex').toUpperCase()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SignRequestParams {
|
|
11
|
+
method: string
|
|
12
|
+
url: string
|
|
13
|
+
timestamp: string
|
|
14
|
+
nonce: string
|
|
15
|
+
body: string
|
|
16
|
+
privateKey: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Constructs the -line signature string and signs it with SHA256withRSA using the merchant private key.
|
|
21
|
+
* Returns Base64 encoded signature.
|
|
22
|
+
*/
|
|
23
|
+
export function signRequest(params: SignRequestParams): string {
|
|
24
|
+
const { method, url, timestamp, nonce, body, privateKey } = params
|
|
25
|
+
const signStr = `${method.toUpperCase()}\n${url}\n${timestamp}\n${nonce}\n${body}\n`
|
|
26
|
+
|
|
27
|
+
const signer = crypto.createSign('RSA-SHA256')
|
|
28
|
+
signer.update(signStr)
|
|
29
|
+
return signer.sign(privateKey, 'base64')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface BuildAuthorizationHeaderParams {
|
|
33
|
+
mchId: string
|
|
34
|
+
serialNo: string
|
|
35
|
+
timestamp: string
|
|
36
|
+
nonce: string
|
|
37
|
+
signature: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds the Authorization header for WeChat Pay V3 API requests.
|
|
42
|
+
*/
|
|
43
|
+
export function buildAuthorizationHeader(params: BuildAuthorizationHeaderParams): string {
|
|
44
|
+
const { mchId, serialNo, timestamp, nonce, signature } = params
|
|
45
|
+
return `WECHATPAY2-SHA256-RSA2048 mchid="${mchId}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${serialNo}",signature="${signature}"`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface VerifySignatureParams {
|
|
49
|
+
timestamp: string
|
|
50
|
+
nonce: string
|
|
51
|
+
body: string
|
|
52
|
+
signature: string
|
|
53
|
+
platformPublicKey: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Verifies WeChat Pay notification signature using the platform public key.
|
|
58
|
+
*/
|
|
59
|
+
export function verifySignature(params: VerifySignatureParams): boolean {
|
|
60
|
+
const { timestamp, nonce, body, signature, platformPublicKey } = params
|
|
61
|
+
const verifyStr = `${timestamp}\n${nonce}\n${body}\n`
|
|
62
|
+
|
|
63
|
+
const verifier = crypto.createVerify('RSA-SHA256')
|
|
64
|
+
verifier.update(verifyStr)
|
|
65
|
+
return verifier.verify(platformPublicKey, signature, 'base64')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface DecryptResourceParams {
|
|
69
|
+
ciphertext: string
|
|
70
|
+
associatedData: string
|
|
71
|
+
nonce: string
|
|
72
|
+
apiV3Key: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decrypts AEAD_AES_256_GCM encrypted resource.
|
|
77
|
+
* The ciphertext includes the auth tag as the last 16 bytes.
|
|
78
|
+
*/
|
|
79
|
+
export function decryptResource(params: DecryptResourceParams): string {
|
|
80
|
+
const { ciphertext, associatedData, nonce, apiV3Key } = params
|
|
81
|
+
const buf = Buffer.from(ciphertext, 'base64')
|
|
82
|
+
|
|
83
|
+
// AES-256-GCM: last 16 bytes are the auth tag
|
|
84
|
+
const authTagLength = 16
|
|
85
|
+
const encrypted = buf.slice(0, -authTagLength)
|
|
86
|
+
const authTag = buf.slice(-authTagLength)
|
|
87
|
+
|
|
88
|
+
const decipher = crypto.createDecipheriv(
|
|
89
|
+
'aes-256-gcm',
|
|
90
|
+
Buffer.from(apiV3Key),
|
|
91
|
+
Buffer.from(nonce, 'utf8'),
|
|
92
|
+
)
|
|
93
|
+
decipher.setAuthTag(authTag)
|
|
94
|
+
decipher.setAAD(Buffer.from(associatedData))
|
|
95
|
+
|
|
96
|
+
let decrypted = decipher.update(encrypted, undefined, 'utf8')
|
|
97
|
+
decrypted += decipher.final('utf8')
|
|
98
|
+
return decrypted
|
|
99
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface WechatPayConfig {
|
|
2
|
+
appId: string
|
|
3
|
+
mchId: string
|
|
4
|
+
privateKey: string // PKCS8 PEM format merchant private key
|
|
5
|
+
serialNo: string // Merchant certificate serial number
|
|
6
|
+
apiV3Key: string // 32-byte APIv3 key
|
|
7
|
+
platformPublicKey: string // WeChat platform public key PEM (downloaded from /v3/certificates)
|
|
8
|
+
baseUrl: string // Domestic: https://api.mch.weixin.qq.com; Overseas: https://apihk.mch.weixin.qq.com
|
|
9
|
+
notifyUrl: string
|
|
10
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
@layer base {
|
|
2
|
+
:root {
|
|
3
|
+
/* Brand tokens — downstream projects override these three variables to change the theme color */
|
|
4
|
+
--color-brand-h: 262;
|
|
5
|
+
--color-brand-c: 0.26;
|
|
6
|
+
--color-brand-l: 0.56;
|
|
7
|
+
|
|
8
|
+
/* Semantic color tokens */
|
|
9
|
+
--color-primary: oklch(var(--color-brand-l) var(--color-brand-c) var(--color-brand-h));
|
|
10
|
+
--color-primary-foreground: oklch(0.98 0 0);
|
|
11
|
+
|
|
12
|
+
/* Neutral surface tokens (with subtle hue, not pure gray) */
|
|
13
|
+
--color-surface: oklch(0.985 0.003 262);
|
|
14
|
+
--color-surface-elevated: oklch(1 0 0);
|
|
15
|
+
--color-muted: oklch(0.94 0.004 262);
|
|
16
|
+
--color-muted-foreground: oklch(0.52 0.012 262);
|
|
17
|
+
--color-border: oklch(0.88 0.006 262);
|
|
18
|
+
|
|
19
|
+
/* Spacing tokens (4px baseline) */
|
|
20
|
+
--space-1: 4px;
|
|
21
|
+
--space-2: 8px;
|
|
22
|
+
--space-3: 12px;
|
|
23
|
+
--space-4: 16px;
|
|
24
|
+
--space-5: 20px;
|
|
25
|
+
--space-6: 24px;
|
|
26
|
+
--space-8: 32px;
|
|
27
|
+
--space-10: 40px;
|
|
28
|
+
--space-12: 48px;
|
|
29
|
+
--space-16: 64px;
|
|
30
|
+
--space-20: 80px;
|
|
31
|
+
--space-24: 96px;
|
|
32
|
+
--space-section: clamp(64px, 10vw, 120px);
|
|
33
|
+
|
|
34
|
+
/* Font tokens */
|
|
35
|
+
--font-heading: var(--font-geist, 'Geist', system-ui, sans-serif);
|
|
36
|
+
--font-body: var(--font-geist, 'Geist', system-ui, sans-serif);
|
|
37
|
+
|
|
38
|
+
/* Text size tokens */
|
|
39
|
+
--text-xs: 0.75rem;
|
|
40
|
+
--text-sm: 0.875rem;
|
|
41
|
+
--text-base: 1rem;
|
|
42
|
+
--text-lg: 1.125rem;
|
|
43
|
+
--text-xl: 1.25rem;
|
|
44
|
+
--text-2xl: 1.5rem;
|
|
45
|
+
--text-3xl: 1.875rem;
|
|
46
|
+
--text-4xl: 2.25rem;
|
|
47
|
+
--text-5xl: 3rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.dark {
|
|
51
|
+
/* Dark mode semantic tokens */
|
|
52
|
+
--color-surface: oklch(0.13 0.008 262);
|
|
53
|
+
--color-surface-elevated: oklch(0.18 0.007 262);
|
|
54
|
+
--color-muted: oklch(0.22 0.007 262);
|
|
55
|
+
--color-muted-foreground: oklch(0.62 0.010 262);
|
|
56
|
+
--color-border: oklch(0.28 0.008 262);
|
|
57
|
+
}
|
|
58
|
+
}
|