@windrun-huaiin/backend-core 10.0.1
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/LICENSE +21 -0
- package/dist/app/api/stripe/checkout/route.d.ts +19 -0
- package/dist/app/api/stripe/checkout/route.d.ts.map +1 -0
- package/dist/app/api/stripe/checkout/route.js +120 -0
- package/dist/app/api/stripe/checkout/route.mjs +118 -0
- package/dist/app/api/stripe/customer-portal/route.d.ts +11 -0
- package/dist/app/api/stripe/customer-portal/route.d.ts.map +1 -0
- package/dist/app/api/stripe/customer-portal/route.js +73 -0
- package/dist/app/api/stripe/customer-portal/route.mjs +71 -0
- package/dist/app/api/user/anonymous/init/route.d.ts +7 -0
- package/dist/app/api/user/anonymous/init/route.d.ts.map +1 -0
- package/dist/app/api/user/anonymous/init/route.js +210 -0
- package/dist/app/api/user/anonymous/init/route.mjs +208 -0
- package/dist/app/api/webhook/clerk/user/route.d.ts +7 -0
- package/dist/app/api/webhook/clerk/user/route.d.ts.map +1 -0
- package/dist/app/api/webhook/clerk/user/route.js +202 -0
- package/dist/app/api/webhook/clerk/user/route.mjs +200 -0
- package/dist/app/api/webhook/stripe/route.d.ts +8 -0
- package/dist/app/api/webhook/stripe/route.d.ts.map +1 -0
- package/dist/app/api/webhook/stripe/route.js +70 -0
- package/dist/app/api/webhook/stripe/route.mjs +67 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.mjs +18 -0
- package/dist/lib/auth-utils.d.ts +46 -0
- package/dist/lib/auth-utils.d.ts.map +1 -0
- package/dist/lib/auth-utils.js +107 -0
- package/dist/lib/auth-utils.mjs +102 -0
- package/dist/lib/credit-init.d.ts +8 -0
- package/dist/lib/credit-init.d.ts.map +1 -0
- package/dist/lib/credit-init.js +16 -0
- package/dist/lib/credit-init.mjs +10 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +31 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/money-price-config.d.ts +51 -0
- package/dist/lib/money-price-config.d.ts.map +1 -0
- package/dist/lib/money-price-config.js +156 -0
- package/dist/lib/money-price-config.mjs +151 -0
- package/dist/lib/stripe-config.d.ts +31 -0
- package/dist/lib/stripe-config.d.ts.map +1 -0
- package/dist/lib/stripe-config.js +278 -0
- package/dist/lib/stripe-config.mjs +268 -0
- package/dist/node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js +48 -0
- package/dist/node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.mjs +45 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.js +54 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.mjs +51 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.js +44 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.mjs +35 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.js +31 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.mjs +18 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.js +587 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.mjs +527 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.js +447 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.mjs +399 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.js +245 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.mjs +232 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.js +68 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.mjs +62 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.js +39 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.mjs +37 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.js +80 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.mjs +75 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.js +101 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.mjs +86 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.js +102 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.mjs +76 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.js +56 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.mjs +52 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.js +1205 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.mjs +1157 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.js +407 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.mjs +374 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.js +9 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.mjs +7 -0
- package/dist/prisma/client.d.ts +2 -0
- package/dist/prisma/client.d.ts.map +1 -0
- package/dist/prisma/client.js +12 -0
- package/dist/prisma/client.mjs +1 -0
- package/dist/prisma/index.d.ts +4 -0
- package/dist/prisma/index.d.ts.map +1 -0
- package/dist/prisma/index.js +10 -0
- package/dist/prisma/index.mjs +2 -0
- package/dist/prisma/prisma-transaction-util.d.ts +3 -0
- package/dist/prisma/prisma-transaction-util.d.ts.map +1 -0
- package/dist/prisma/prisma-transaction-util.js +29 -0
- package/dist/prisma/prisma-transaction-util.mjs +27 -0
- package/dist/prisma/prisma.d.ts +4 -0
- package/dist/prisma/prisma.d.ts.map +1 -0
- package/dist/prisma/prisma.js +109 -0
- package/dist/prisma/prisma.mjs +106 -0
- package/dist/services/aggregate/billing.aggregate.service.d.ts +83 -0
- package/dist/services/aggregate/billing.aggregate.service.d.ts.map +1 -0
- package/dist/services/aggregate/billing.aggregate.service.js +308 -0
- package/dist/services/aggregate/billing.aggregate.service.mjs +306 -0
- package/dist/services/aggregate/index.d.ts +3 -0
- package/dist/services/aggregate/index.d.ts.map +1 -0
- package/dist/services/aggregate/index.js +9 -0
- package/dist/services/aggregate/index.mjs +2 -0
- package/dist/services/aggregate/user.aggregate.service.d.ts +34 -0
- package/dist/services/aggregate/user.aggregate.service.d.ts.map +1 -0
- package/dist/services/aggregate/user.aggregate.service.js +136 -0
- package/dist/services/aggregate/user.aggregate.service.mjs +133 -0
- package/dist/services/context/index.d.ts +2 -0
- package/dist/services/context/index.d.ts.map +1 -0
- package/dist/services/context/index.js +13 -0
- package/dist/services/context/index.mjs +1 -0
- package/dist/services/context/user-context-service.d.ts +30 -0
- package/dist/services/context/user-context-service.d.ts.map +1 -0
- package/dist/services/context/user-context-service.js +170 -0
- package/dist/services/context/user-context-service.mjs +162 -0
- package/dist/services/database/apilog.service.d.ts +39 -0
- package/dist/services/database/apilog.service.d.ts.map +1 -0
- package/dist/services/database/apilog.service.js +174 -0
- package/dist/services/database/apilog.service.mjs +170 -0
- package/dist/services/database/constants.d.ts +73 -0
- package/dist/services/database/constants.d.ts.map +1 -0
- package/dist/services/database/constants.js +135 -0
- package/dist/services/database/constants.mjs +117 -0
- package/dist/services/database/credit.service.d.ts +107 -0
- package/dist/services/database/credit.service.d.ts.map +1 -0
- package/dist/services/database/credit.service.js +515 -0
- package/dist/services/database/credit.service.mjs +512 -0
- package/dist/services/database/creditAuditLog.service.d.ts +73 -0
- package/dist/services/database/creditAuditLog.service.d.ts.map +1 -0
- package/dist/services/database/creditAuditLog.service.js +305 -0
- package/dist/services/database/creditAuditLog.service.mjs +302 -0
- package/dist/services/database/index.d.ts +10 -0
- package/dist/services/database/index.d.ts.map +1 -0
- package/dist/services/database/index.js +38 -0
- package/dist/services/database/index.mjs +8 -0
- package/dist/services/database/prisma-model-type.d.ts +3 -0
- package/dist/services/database/prisma-model-type.d.ts.map +1 -0
- package/dist/services/database/subscription.service.d.ts +48 -0
- package/dist/services/database/subscription.service.d.ts.map +1 -0
- package/dist/services/database/subscription.service.js +267 -0
- package/dist/services/database/subscription.service.mjs +264 -0
- package/dist/services/database/transaction.service.d.ts +92 -0
- package/dist/services/database/transaction.service.d.ts.map +1 -0
- package/dist/services/database/transaction.service.js +326 -0
- package/dist/services/database/transaction.service.mjs +323 -0
- package/dist/services/database/user.service.d.ts +45 -0
- package/dist/services/database/user.service.d.ts.map +1 -0
- package/dist/services/database/user.service.js +180 -0
- package/dist/services/database/user.service.mjs +177 -0
- package/dist/services/database/userBackup.service.d.ts +45 -0
- package/dist/services/database/userBackup.service.d.ts.map +1 -0
- package/dist/services/database/userBackup.service.js +249 -0
- package/dist/services/database/userBackup.service.mjs +246 -0
- package/dist/services/stripe/index.d.ts +2 -0
- package/dist/services/stripe/index.d.ts.map +1 -0
- package/dist/services/stripe/index.js +7 -0
- package/dist/services/stripe/index.mjs +1 -0
- package/dist/services/stripe/webhook-handler.d.ts +6 -0
- package/dist/services/stripe/webhook-handler.d.ts.map +1 -0
- package/dist/services/stripe/webhook-handler.js +537 -0
- package/dist/services/stripe/webhook-handler.mjs +535 -0
- package/migrations/create.sql +176 -0
- package/migrations/db.init.sql +13 -0
- package/migrations/init-schema.sql +19 -0
- package/migrations/purge.sql +27 -0
- package/migrations/test-check.sql +167 -0
- package/package.json +123 -0
- package/prisma/schema.prisma +191 -0
- package/src/app/api/stripe/checkout/route.ts +145 -0
- package/src/app/api/stripe/customer-portal/route.ts +83 -0
- package/src/app/api/user/anonymous/init/route.ts +284 -0
- package/src/app/api/webhook/clerk/user/route.ts +249 -0
- package/src/app/api/webhook/stripe/route.ts +93 -0
- package/src/index.ts +6 -0
- package/src/lib/auth-utils.ts +101 -0
- package/src/lib/credit-init.ts +9 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/money-price-config.ts +168 -0
- package/src/lib/stripe-config.ts +333 -0
- package/src/prisma/client.ts +2 -0
- package/src/prisma/index.ts +3 -0
- package/src/prisma/prisma-transaction-util.ts +24 -0
- package/src/prisma/prisma.ts +122 -0
- package/src/services/aggregate/billing.aggregate.service.ts +498 -0
- package/src/services/aggregate/index.ts +2 -0
- package/src/services/aggregate/user.aggregate.service.ts +168 -0
- package/src/services/context/index.ts +1 -0
- package/src/services/context/user-context-service.ts +200 -0
- package/src/services/database/apilog.service.ts +185 -0
- package/src/services/database/constants.ts +148 -0
- package/src/services/database/credit.service.ts +747 -0
- package/src/services/database/creditAuditLog.service.ts +402 -0
- package/src/services/database/index.ts +41 -0
- package/src/services/database/prisma-model-type.ts +13 -0
- package/src/services/database/subscription.service.ts +319 -0
- package/src/services/database/transaction.service.ts +447 -0
- package/src/services/database/user.service.ts +218 -0
- package/src/services/database/userBackup.service.ts +290 -0
- package/src/services/stripe/index.ts +1 -0
- package/src/services/stripe/webhook-handler.ts +648 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Fix BigInt serialization issue
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
|
+
(BigInt.prototype as any).toJSON = function () {
|
|
4
|
+
return this.toString();
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
import { userAggregateService } from '../../../../../services/aggregate/index';
|
|
8
|
+
import { UserStatus } from '../../../../../services//database/constants';
|
|
9
|
+
import { Apilogger, userService } from '../../../../../services//database/index';
|
|
10
|
+
import { headers } from 'next/headers';
|
|
11
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
12
|
+
import { Webhook } from 'svix';
|
|
13
|
+
|
|
14
|
+
// 定义Clerk Webhook事件类型
|
|
15
|
+
interface ClerkWebhookEvent {
|
|
16
|
+
data: {
|
|
17
|
+
id: string;
|
|
18
|
+
email_addresses?: Array<{
|
|
19
|
+
email_address: string;
|
|
20
|
+
}>;
|
|
21
|
+
first_name?: string;
|
|
22
|
+
last_name?: string,
|
|
23
|
+
username?: string,
|
|
24
|
+
unsafe_metadata?: {
|
|
25
|
+
user_id?: string;
|
|
26
|
+
fingerprint_id?: string;
|
|
27
|
+
};
|
|
28
|
+
deleted?: boolean;
|
|
29
|
+
object?: string;
|
|
30
|
+
};
|
|
31
|
+
event_attributes?: {
|
|
32
|
+
http_request?: {
|
|
33
|
+
client_ip?: string;
|
|
34
|
+
user_agent?: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
instance_id?: string;
|
|
38
|
+
object: string;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
type: 'user.created' | 'user.deleted';
|
|
41
|
+
}
|
|
42
|
+
export async function POST(request: NextRequest) {
|
|
43
|
+
try {
|
|
44
|
+
// 获取原始请求体
|
|
45
|
+
const rawBody = await request.text();
|
|
46
|
+
|
|
47
|
+
let event: ClerkWebhookEvent;
|
|
48
|
+
|
|
49
|
+
// 开发环境跳过签名校验
|
|
50
|
+
if (process.env.NODE_ENV === 'development') {
|
|
51
|
+
console.log('Development mode: skipping webhook signature verification');
|
|
52
|
+
try {
|
|
53
|
+
event = JSON.parse(rawBody) as ClerkWebhookEvent;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('Failed to parse webhook body:', err);
|
|
56
|
+
return NextResponse.json(
|
|
57
|
+
{ error: 'Invalid webhook body' },
|
|
58
|
+
{ status: 400 }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// 生产环境进行签名校验
|
|
63
|
+
const headerPayload = await headers();
|
|
64
|
+
const svix_id = headerPayload.get('svix-id');
|
|
65
|
+
const svix_timestamp = headerPayload.get('svix-timestamp');
|
|
66
|
+
const svix_signature = headerPayload.get('svix-signature');
|
|
67
|
+
|
|
68
|
+
// 如果缺少必要的header,返回错误
|
|
69
|
+
if (!svix_id || !svix_timestamp || !svix_signature) {
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{ error: 'Missing webhook headers' },
|
|
72
|
+
{ status: 400 }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 获取webhook signing secret
|
|
77
|
+
const webhookSecret = process.env.CLERK_WEBHOOK_SECRET;
|
|
78
|
+
if (!webhookSecret) {
|
|
79
|
+
console.error('CLERK_WEBHOOK_SECRET is not configured');
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: 'Webhook configuration error' },
|
|
82
|
+
{ status: 500 }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 验证webhook签名
|
|
87
|
+
try {
|
|
88
|
+
const wh = new Webhook(webhookSecret);
|
|
89
|
+
event = wh.verify(rawBody, {
|
|
90
|
+
'svix-id': svix_id,
|
|
91
|
+
'svix-timestamp': svix_timestamp,
|
|
92
|
+
'svix-signature': svix_signature,
|
|
93
|
+
}) as ClerkWebhookEvent;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('Webhook signature verification failed:', err);
|
|
96
|
+
return NextResponse.json(
|
|
97
|
+
{ error: 'Webhook signature verification failed' },
|
|
98
|
+
{ status: 400 }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Log the incoming webhook
|
|
104
|
+
const logId = await Apilogger.logClerkIncoming(`${event.type}`, {
|
|
105
|
+
clerk_user_id: event.data.id,
|
|
106
|
+
email: event.data.email_addresses?.[0]?.email_address,
|
|
107
|
+
fingerprint_id: event.data.unsafe_metadata?.fingerprint_id
|
|
108
|
+
}, event);
|
|
109
|
+
|
|
110
|
+
let processingResult = { success: true, message: 'Event processed successfully' };
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// 处理不同的事件类型
|
|
114
|
+
const { type } = event;
|
|
115
|
+
|
|
116
|
+
switch (type) {
|
|
117
|
+
case 'user.created':
|
|
118
|
+
await handleUserCreated(event);
|
|
119
|
+
break;
|
|
120
|
+
case 'user.deleted':
|
|
121
|
+
await handleUserDeleted(event);
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
console.log(`Unhandled event type: ${type}`);
|
|
125
|
+
processingResult = { success: false, message: `Unhandled event type: ${type}`};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update response in log
|
|
129
|
+
Apilogger.updateResponse(logId, processingResult);
|
|
130
|
+
|
|
131
|
+
return NextResponse.json({ received: true });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Webhook processing error:', error);
|
|
134
|
+
|
|
135
|
+
// Update error response in log
|
|
136
|
+
const errorResult = {
|
|
137
|
+
success: false,
|
|
138
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
139
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
140
|
+
};
|
|
141
|
+
Apilogger.updateResponse(logId, errorResult);
|
|
142
|
+
|
|
143
|
+
return NextResponse.json(
|
|
144
|
+
{ error: 'Internal server error' },
|
|
145
|
+
{ status: 500 }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Webhook processing error:', error);
|
|
150
|
+
return NextResponse.json(
|
|
151
|
+
{ error: 'Internal server error' },
|
|
152
|
+
{ status: 500 }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 处理用户创建事件
|
|
159
|
+
*/
|
|
160
|
+
async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
161
|
+
const { data } = event;
|
|
162
|
+
const clerkUserId = data.id;
|
|
163
|
+
const email = data.email_addresses?.[0]?.email_address;
|
|
164
|
+
const unsafeMetadata = data.unsafe_metadata;
|
|
165
|
+
const fingerprintId = unsafeMetadata?.fingerprint_id;
|
|
166
|
+
const userName = data.username
|
|
167
|
+
? data.username
|
|
168
|
+
: [data.first_name, data.last_name].filter(Boolean).join(' ') || undefined;
|
|
169
|
+
|
|
170
|
+
console.log('Processing user.created event:', {
|
|
171
|
+
clerkUserId,
|
|
172
|
+
email,
|
|
173
|
+
fingerprintId,
|
|
174
|
+
userName
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 检查必要参数
|
|
178
|
+
if (!fingerprintId) {
|
|
179
|
+
console.error('Missing fingerprintId in webhook data, process flow error');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!email) {
|
|
184
|
+
console.error('Missing email in webhook data');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// 按fingerprintId查询该设备的所有未注销过的用户记录,注销过的记录相当于是死数据
|
|
190
|
+
const existingUsers = await userService.findListByFingerprintId(fingerprintId);
|
|
191
|
+
if (!existingUsers || existingUsers.length === 0) {
|
|
192
|
+
console.error('Invalid fingerprintId in webhook data, process flow error');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 查找email相同的记录
|
|
197
|
+
const sameEmailUser = existingUsers.find(user => user.email === email);
|
|
198
|
+
if (sameEmailUser) {
|
|
199
|
+
// 同一账号,检查是否需要更新clerkUserId
|
|
200
|
+
if (sameEmailUser.clerkUserId !== clerkUserId) {
|
|
201
|
+
await userService.updateUser(sameEmailUser.userId, { clerkUserId, userName: userName, status: UserStatus.REGISTERED });
|
|
202
|
+
console.log(`Updated clerkUserId for user ${sameEmailUser.userId}`);
|
|
203
|
+
} else {
|
|
204
|
+
console.log(`User with email ${email} already exists, skipping duplicate message`);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 查找匿名用户(email为空且clerkUserId为空)
|
|
210
|
+
const anonymousUser = existingUsers.find(user => !user.email && !user.clerkUserId && user.status === UserStatus.ANONYMOUS );
|
|
211
|
+
if (anonymousUser) {
|
|
212
|
+
// 匿名用户升级
|
|
213
|
+
await userAggregateService.upgradeToRegistered(anonymousUser.userId, email, clerkUserId, userName);
|
|
214
|
+
console.log(`Successfully upgraded anonymous user ${anonymousUser.userId} to registered user`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 同设备新账号,创建新用户
|
|
219
|
+
await userAggregateService.createNewRegisteredUser(clerkUserId, email, fingerprintId, userName);
|
|
220
|
+
console.log(`Created new user for device ${fingerprintId} with email ${email}`);
|
|
221
|
+
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('Error handling user.created event:', error);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 处理用户删除事件
|
|
230
|
+
*/
|
|
231
|
+
async function handleUserDeleted(event: ClerkWebhookEvent) {
|
|
232
|
+
const { data } = event;
|
|
233
|
+
const clerkUserId = data.id;
|
|
234
|
+
|
|
235
|
+
console.log('Processing user.deleted event:', { clerkUserId });
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const userId = await userAggregateService.handleUserUnregister(clerkUserId);
|
|
239
|
+
if (!userId) {
|
|
240
|
+
console.warn(`User not found, skipping oprate , (clerkUserId: ${clerkUserId})`);
|
|
241
|
+
} else {
|
|
242
|
+
console.log(`Successfully deleted user ${userId} , (clerkUserId: ${clerkUserId})`);
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('Error handling user.deleted event:', error);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { headers } from 'next/headers';
|
|
3
|
+
import { validateStripeWebhook } from '../../../../lib/stripe-config';
|
|
4
|
+
import Stripe from 'stripe';
|
|
5
|
+
import { Apilogger } from '../../../../services/database/index';
|
|
6
|
+
import { handleStripeEvent } from '../../../../services//stripe/webhook-handler';
|
|
7
|
+
|
|
8
|
+
// Disable body parsing, need raw body for signature verification
|
|
9
|
+
export const runtime = 'nodejs';
|
|
10
|
+
|
|
11
|
+
export async function POST(request: NextRequest) {
|
|
12
|
+
try {
|
|
13
|
+
// 1. Get raw request body and signature
|
|
14
|
+
const body = await request.text();
|
|
15
|
+
const headersList = await headers();
|
|
16
|
+
const signature = headersList.get('stripe-signature');
|
|
17
|
+
|
|
18
|
+
if (!signature) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: 'Missing stripe-signature header' },
|
|
21
|
+
{ status: 400 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!process.env.STRIPE_WEBHOOK_SECRET) {
|
|
26
|
+
console.error('STRIPE_WEBHOOK_SECRET is not configured');
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ error: 'Webhook configuration error' },
|
|
29
|
+
{ status: 500 }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Validate webhook signature
|
|
34
|
+
const event = validateStripeWebhook(
|
|
35
|
+
body,
|
|
36
|
+
signature,
|
|
37
|
+
process.env.STRIPE_WEBHOOK_SECRET
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
console.log(`Received webhook event: ${event.type} | ID: ${event.id}`);
|
|
41
|
+
|
|
42
|
+
// 3. Log incoming webhook and capture log ID for later updates
|
|
43
|
+
const logId = await Apilogger.logStripeIncoming(event.type, event.id, event);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// 4. Handle the event
|
|
47
|
+
await handleStripeEvent(event);
|
|
48
|
+
|
|
49
|
+
// 5. Update log with success response
|
|
50
|
+
const processingResult = {
|
|
51
|
+
success: true,
|
|
52
|
+
message: 'Event processed successfully'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
Apilogger.updateResponse(logId, processingResult);
|
|
56
|
+
|
|
57
|
+
return NextResponse.json({ received: true });
|
|
58
|
+
} catch (handlerError) {
|
|
59
|
+
console.error('Stripe webhook processing error:', handlerError);
|
|
60
|
+
|
|
61
|
+
const errorResult = {
|
|
62
|
+
success: false,
|
|
63
|
+
error:
|
|
64
|
+
handlerError instanceof Error
|
|
65
|
+
? handlerError.message
|
|
66
|
+
: 'Unknown error',
|
|
67
|
+
stack: handlerError instanceof Error ? handlerError.stack : undefined,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
Apilogger.updateResponse(logId, errorResult);
|
|
71
|
+
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{ error: 'Webhook processing failed' },
|
|
74
|
+
{ status: 500 }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Webhook error:', error);
|
|
80
|
+
|
|
81
|
+
if (error instanceof Stripe.errors.StripeSignatureVerificationError) {
|
|
82
|
+
return NextResponse.json(
|
|
83
|
+
{ error: 'Invalid signature' },
|
|
84
|
+
{ status: 400 }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{ error: 'Webhook processing failed' },
|
|
90
|
+
{ status: 500 }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { userService } from '../services/database/index';
|
|
3
|
+
import { User } from '../services/database/prisma-model-type';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 认证结果类型
|
|
7
|
+
*/
|
|
8
|
+
export interface AuthResult {
|
|
9
|
+
userId: string;
|
|
10
|
+
user: User;
|
|
11
|
+
clerkUserId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 从中间件设置的 Clerk ID 获取完整用户信息
|
|
16
|
+
*/
|
|
17
|
+
export async function getAuthenticatedUser(req: NextRequest): Promise<AuthResult> {
|
|
18
|
+
try {
|
|
19
|
+
const clerkUserId = req.headers.get('x-clerk-user-id');
|
|
20
|
+
if (!clerkUserId) {
|
|
21
|
+
throw new Error('Authentication required');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const user = await userService.findByClerkUserId(clerkUserId);
|
|
25
|
+
if (!user) {
|
|
26
|
+
throw new Error('User not found in database');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
userId: user.userId,
|
|
31
|
+
user,
|
|
32
|
+
clerkUserId
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error getting authenticated user:', error);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 要求用户必须已认证,返回用户ID
|
|
42
|
+
*/
|
|
43
|
+
export async function requireAuth(req: NextRequest): Promise<string> {
|
|
44
|
+
const auth = await getAuthenticatedUser(req);
|
|
45
|
+
return auth.userId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 要求用户必须已认证,返回完整用户信息
|
|
50
|
+
*/
|
|
51
|
+
export async function requireAuthWithUser(req: NextRequest): Promise<AuthResult> {
|
|
52
|
+
return await getAuthenticatedUser(req);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* API Route版本的认证工具函数
|
|
57
|
+
*/
|
|
58
|
+
export class ApiAuthUtils {
|
|
59
|
+
private req: NextRequest;
|
|
60
|
+
|
|
61
|
+
constructor(req: NextRequest) {
|
|
62
|
+
this.req = req;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 要求用户必须已认证,返回用户ID
|
|
67
|
+
*/
|
|
68
|
+
async requireAuth(): Promise<string> {
|
|
69
|
+
return await requireAuth(this.req);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 要求用户必须已认证,返回完整用户信息
|
|
74
|
+
*/
|
|
75
|
+
async requireAuthWithUser(): Promise<AuthResult> {
|
|
76
|
+
return await requireAuthWithUser(this.req);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 获取用户ID(如果已认证)
|
|
81
|
+
*/
|
|
82
|
+
async getUserId(): Promise<string | null> {
|
|
83
|
+
try {
|
|
84
|
+
const auth = await getAuthenticatedUser(this.req);
|
|
85
|
+
return auth.userId;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 获取完整用户信息(如果已认证)
|
|
93
|
+
*/
|
|
94
|
+
async getUser(): Promise<AuthResult | null> {
|
|
95
|
+
try {
|
|
96
|
+
return await getAuthenticatedUser(this.req);
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// create app config
|
|
2
|
+
export const creditsConfig = {
|
|
3
|
+
freeAmount: Number(process.env.CREDITS_INIT_FREE_AMOUNT) || 1,
|
|
4
|
+
freeRegisterAmount: Number(process.env.CREDITS_INIT_FREE_REGISTER_AMOUNT) || 2,
|
|
5
|
+
freeExpiredDays: Number(process.env.CREDITS_INIT_FREE_EXPIRED_DAYS) || 7,
|
|
6
|
+
oneTimeExpiredDays: Number(process.env.CREDITS_ONE_TIME_EXPIRED_DAYS) || 30
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const { freeAmount, freeRegisterAmount, freeExpiredDays, oneTimeExpiredDays } = creditsConfig;
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MoneyPriceConfig,
|
|
3
|
+
PaymentProviderConfig,
|
|
4
|
+
EnhancePricePlan,
|
|
5
|
+
getActiveProviderConfigUtil,
|
|
6
|
+
getCreditsFromPriceIdUtil,
|
|
7
|
+
getPriceConfigUtil
|
|
8
|
+
} from '@windrun-huaiin/third-ui/main/server'
|
|
9
|
+
|
|
10
|
+
export const moneyPriceConfig: MoneyPriceConfig = {
|
|
11
|
+
paymentProviders: {
|
|
12
|
+
stripe: {
|
|
13
|
+
provider: 'stripe',
|
|
14
|
+
enabled: true,
|
|
15
|
+
// 订阅模式产品
|
|
16
|
+
subscriptionProducts: {
|
|
17
|
+
F1: {
|
|
18
|
+
key: 'F1',
|
|
19
|
+
plans: {
|
|
20
|
+
monthly: {
|
|
21
|
+
priceId: 'free',
|
|
22
|
+
amount: 0,
|
|
23
|
+
currency: 'usd',
|
|
24
|
+
credits: 0
|
|
25
|
+
},
|
|
26
|
+
yearly: {
|
|
27
|
+
priceId: 'free',
|
|
28
|
+
amount: 0,
|
|
29
|
+
currency: 'usd',
|
|
30
|
+
credits: 0
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
P2: {
|
|
35
|
+
key: 'P2',
|
|
36
|
+
plans: {
|
|
37
|
+
monthly: {
|
|
38
|
+
priceId: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!,
|
|
39
|
+
amount: Number(process.env.STRIPE_PRO_MONTHLY_AMOUNT!), // 10
|
|
40
|
+
currency: process.env.STRIPE_PRO_MONTHLY_CURRENCY!,
|
|
41
|
+
credits: Number(process.env.STRIPE_PRO_MONTHLY_CREDITS!)
|
|
42
|
+
},
|
|
43
|
+
yearly: {
|
|
44
|
+
priceId: process.env.STRIPE_PRO_YEARLY_PRICE_ID!,
|
|
45
|
+
amount: Number(process.env.STRIPE_PRO_YEARLY_AMOUNT!),
|
|
46
|
+
originalAmount: Number(process.env.STRIPE_PRO_MONTHLY_AMOUNT!), // 10,
|
|
47
|
+
discountPercent: Number(process.env.STRIPE_PRO_DISCOUNT_PERCENT),
|
|
48
|
+
currency: process.env.STRIPE_PRO_YEARLY_CURRENCY!,
|
|
49
|
+
credits: Number(process.env.STRIPE_PRO_YEARLY_CREDITS!)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
U3: {
|
|
54
|
+
key: 'U3',
|
|
55
|
+
plans: {
|
|
56
|
+
monthly: {
|
|
57
|
+
priceId: process.env.STRIPE_ULTRA_MONTHLY_PRICE_ID!,
|
|
58
|
+
amount: Number(process.env.STRIPE_ULTRA_MONTHLY_AMOUNT!),
|
|
59
|
+
currency: process.env.STRIPE_ULTRA_MONTHLY_CURRENCY!,
|
|
60
|
+
credits: Number(process.env.STRIPE_ULTRA_MONTHLY_CREDITS!)
|
|
61
|
+
},
|
|
62
|
+
yearly: {
|
|
63
|
+
priceId: process.env.STRIPE_ULTRA_YEARLY_PRICE_ID!,
|
|
64
|
+
amount: Number(process.env.STRIPE_ULTRA_YEARLY_AMOUNT!),
|
|
65
|
+
originalAmount: Number(process.env.STRIPE_ULTRA_MONTHLY_AMOUNT!),
|
|
66
|
+
discountPercent: Number(process.env.STRIPE_ULTRA_DISCOUNT_PERCENT),
|
|
67
|
+
currency: process.env.STRIPE_ULTRA_YEARLY_CURRENCY!,
|
|
68
|
+
credits: Number(process.env.STRIPE_ULTRA_YEARLY_CREDITS!)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
// 积分包产品
|
|
74
|
+
creditPackProducts: {
|
|
75
|
+
F1: {
|
|
76
|
+
key: 'F1',
|
|
77
|
+
priceId: process.env.STRIPE_ONE_TIME_LESS_PRICE_ID!,
|
|
78
|
+
amount: Number(process.env.STRIPE_ONE_TIME_LESS_AMOUNT!),
|
|
79
|
+
currency: process.env.STRIPE_ONE_TIME_LESS_CURRENCY!,
|
|
80
|
+
credits: Number(process.env.STRIPE_ONE_TIME_LESS_CREDITS!)
|
|
81
|
+
},
|
|
82
|
+
P2: {
|
|
83
|
+
key: 'P2',
|
|
84
|
+
priceId: process.env.STRIPE_ONE_TIME_MID_PRICE_ID!,
|
|
85
|
+
amount: Number(process.env.STRIPE_ONE_TIME_MID_AMOUNT!),
|
|
86
|
+
currency: process.env.STRIPE_ONE_TIME_MID_CURRENCY!,
|
|
87
|
+
credits: Number(process.env.STRIPE_ONE_TIME_MID_CREDITS!)
|
|
88
|
+
},
|
|
89
|
+
U3: {
|
|
90
|
+
key: 'U3',
|
|
91
|
+
priceId: process.env.STRIPE_ONE_TIME_MORE_PRICE_ID!,
|
|
92
|
+
amount: Number(process.env.STRIPE_ONE_TIME_MORE_AMOUNT!),
|
|
93
|
+
currency: process.env.STRIPE_ONE_TIME_MORE_CURRENCY!,
|
|
94
|
+
credits: Number(process.env.STRIPE_ONE_TIME_MORE_CREDITS!)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
activeProvider: process.env.ACTIVE_PAYMENT_PROVIDER || 'stripe',
|
|
101
|
+
|
|
102
|
+
display: {
|
|
103
|
+
currency: '$',
|
|
104
|
+
locale: 'en',
|
|
105
|
+
minFeaturesCount: 4
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ============ 应用层wrapper - 隐藏moneyPriceConfig细节 ============
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 获取当前激活的支付供应商配置
|
|
113
|
+
*
|
|
114
|
+
* 🔒 安全设计:
|
|
115
|
+
* - wrapper函数隐藏moneyPriceConfig
|
|
116
|
+
* - util层负责从config中提取激活的provider配置
|
|
117
|
+
* - 外部只能通过这个wrapper访问,看不到config对象
|
|
118
|
+
*
|
|
119
|
+
* @returns 当前激活的支付供应商配置
|
|
120
|
+
*/
|
|
121
|
+
export function getActiveProviderConfig(): PaymentProviderConfig {
|
|
122
|
+
return getActiveProviderConfigUtil(moneyPriceConfig);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 根据 priceId 获取对应的积分数量
|
|
127
|
+
*
|
|
128
|
+
* 🔒 安全设计:
|
|
129
|
+
* - wrapper函数隐藏moneyPriceConfig
|
|
130
|
+
* - util层负责解析config并提取结果
|
|
131
|
+
* - 外部只能通过这个wrapper访问,看不到config对象
|
|
132
|
+
*
|
|
133
|
+
* @param priceId - 查询的价格ID
|
|
134
|
+
* @param _provider - 保留参数(向后兼容),暂未使用
|
|
135
|
+
* @returns 对应的积分数量,或null
|
|
136
|
+
*/
|
|
137
|
+
export function getCreditsFromPriceId(priceId?: string, _provider?: string): number | null {
|
|
138
|
+
return getCreditsFromPriceIdUtil(priceId, moneyPriceConfig);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 根据查询参数获取价格配置
|
|
143
|
+
*
|
|
144
|
+
* 支持三种查询方式:
|
|
145
|
+
* 1. 按 priceId 查询:getPriceConfig(priceId='price_xxx')
|
|
146
|
+
* 2. 按 plan 和 billingType 查询:getPriceConfig(undefined, 'P2', 'monthly')
|
|
147
|
+
* 3. 按 plan 查询:getPriceConfig(undefined, 'P2')
|
|
148
|
+
*
|
|
149
|
+
* 🔒 安全设计:
|
|
150
|
+
* - wrapper函数隐藏moneyPriceConfig
|
|
151
|
+
* - util层负责解析config并提取匹配的结果
|
|
152
|
+
* - 外部只能通过这个wrapper访问,看不到config对象
|
|
153
|
+
*
|
|
154
|
+
* @param priceId - 查询的价格ID(可选)
|
|
155
|
+
* @param plan - 查询的套餐名称如'P2'、'U3'(可选)
|
|
156
|
+
* @param billingType - 查询的计费类型如'monthly'、'yearly'(可选)
|
|
157
|
+
* @param _provider - 保留参数(向后兼容),暂未使用
|
|
158
|
+
* @returns 匹配的价格配置,包含计算好的元数据(priceName、description、interval)
|
|
159
|
+
*/
|
|
160
|
+
export function getPriceConfig(
|
|
161
|
+
priceId?: string,
|
|
162
|
+
plan?: string,
|
|
163
|
+
billingType?: string,
|
|
164
|
+
_provider?: string
|
|
165
|
+
): (EnhancePricePlan & { priceName: string; description: string; interval?: string }) | null {
|
|
166
|
+
return getPriceConfigUtil(priceId, plan, billingType, moneyPriceConfig);
|
|
167
|
+
}
|
|
168
|
+
|