@windrun-huaiin/backend-core 29.0.3 → 31.0.0
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 +95 -0
- package/dist/app/api/user/anonymous/init/route.d.ts +1 -1
- package/dist/app/api/user/anonymous/init/route.d.ts.map +1 -1
- package/dist/app/api/user/anonymous/init/route.js +18 -19
- package/dist/app/api/user/anonymous/init/route.mjs +18 -19
- package/dist/app/api/webhook/clerk/user/route.js +16 -16
- package/dist/app/api/webhook/clerk/user/route.mjs +16 -16
- package/dist/auth/auth-utils.d.ts +8 -23
- package/dist/auth/auth-utils.d.ts.map +1 -1
- package/dist/auth/auth-utils.js +8 -20
- package/dist/auth/auth-utils.mjs +8 -20
- package/dist/lib/money-price-config.d.ts +28 -28
- package/dist/lib/money-price-config.js +31 -31
- package/dist/lib/money-price-config.mjs +31 -31
- package/dist/lib/stripe-config.js +3 -3
- package/dist/lib/stripe-config.mjs +3 -3
- package/dist/prisma/prisma-transaction-util.js +1 -1
- package/dist/prisma/prisma-transaction-util.mjs +1 -1
- package/dist/prisma/prisma.d.ts.map +1 -1
- package/dist/prisma/prisma.js +18 -19
- package/dist/prisma/prisma.mjs +18 -19
- package/dist/services/aggregate/billing.aggregate.service.js +6 -6
- package/dist/services/aggregate/billing.aggregate.service.mjs +6 -6
- package/dist/services/aggregate/user.aggregate.service.d.ts +9 -9
- package/dist/services/aggregate/user.aggregate.service.js +16 -16
- package/dist/services/aggregate/user.aggregate.service.mjs +16 -16
- package/dist/services/database/constants.js +34 -34
- package/dist/services/database/constants.mjs +34 -34
- package/dist/services/database/credit.service.js +2 -2
- package/dist/services/database/credit.service.mjs +2 -2
- package/dist/services/database/transaction.service.js +1 -1
- package/dist/services/database/transaction.service.mjs +1 -1
- package/dist/services/database/user.service.js +2 -2
- package/dist/services/database/user.service.mjs +2 -2
- package/dist/services/stripe/webhook-handler.js +5 -5
- package/dist/services/stripe/webhook-handler.mjs +5 -5
- package/package.json +13 -6
- package/src/app/api/user/anonymous/init/route.ts +21 -22
- package/src/app/api/webhook/clerk/user/route.ts +17 -17
- package/src/auth/auth-utils.ts +8 -23
- package/src/lib/money-price-config.ts +31 -32
- package/src/lib/stripe-config.ts +3 -3
- package/src/prisma/prisma-transaction-util.ts +1 -1
- package/src/prisma/prisma.ts +18 -19
- package/src/services/aggregate/billing.aggregate.service.ts +7 -7
- package/src/services/aggregate/user.aggregate.service.ts +16 -16
- package/src/services/database/constants.ts +34 -34
- package/src/services/database/credit.service.ts +2 -2
- package/src/services/database/transaction.service.ts +1 -1
- package/src/services/database/user.service.ts +2 -2
- package/src/services/stripe/webhook-handler.ts +5 -5
|
@@ -16,7 +16,7 @@ class TransactionService {
|
|
|
16
16
|
orderId: data.orderId,
|
|
17
17
|
orderStatus: data.orderStatus || constants.OrderStatus.CREATED,
|
|
18
18
|
paymentStatus: data.paymentStatus || constants.PaymentStatus.UN_PAID,
|
|
19
|
-
orderExpiredAt: data.orderExpiredAt || new Date(Date.now() + 30 * 60 * 1000), //
|
|
19
|
+
orderExpiredAt: data.orderExpiredAt || new Date(Date.now() + 30 * 60 * 1000), // Default expiration: 30 minutes
|
|
20
20
|
paySupplier: data.paySupplier,
|
|
21
21
|
payTransactionId: data.payTransactionId,
|
|
22
22
|
paySubscriptionId: data.paySubscriptionId,
|
|
@@ -14,7 +14,7 @@ class TransactionService {
|
|
|
14
14
|
orderId: data.orderId,
|
|
15
15
|
orderStatus: data.orderStatus || OrderStatus.CREATED,
|
|
16
16
|
paymentStatus: data.paymentStatus || PaymentStatus.UN_PAID,
|
|
17
|
-
orderExpiredAt: data.orderExpiredAt || new Date(Date.now() + 30 * 60 * 1000), //
|
|
17
|
+
orderExpiredAt: data.orderExpiredAt || new Date(Date.now() + 30 * 60 * 1000), // Default expiration: 30 minutes
|
|
18
18
|
paySupplier: data.paySupplier,
|
|
19
19
|
payTransactionId: data.payTransactionId,
|
|
20
20
|
paySubscriptionId: data.paySubscriptionId,
|
|
@@ -63,7 +63,7 @@ class UserService {
|
|
|
63
63
|
findByClerkUserId(clerkUserId, tx) {
|
|
64
64
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
|
65
65
|
const client = prisma.checkAndFallbackWithNonTCClient(tx);
|
|
66
|
-
// DB
|
|
66
|
+
// Partial DB indexes match this status filter, so findUnique is valid here.
|
|
67
67
|
return yield client.user.findUnique({
|
|
68
68
|
where: {
|
|
69
69
|
clerkUserId,
|
|
@@ -137,7 +137,7 @@ class UserService {
|
|
|
137
137
|
return { users, total };
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
|
-
//
|
|
140
|
+
// Create anonymous users in bulk
|
|
141
141
|
createBatchAnonymousUsers(fingerprintIds, tx) {
|
|
142
142
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
|
143
143
|
const client = prisma.checkAndFallbackWithNonTCClient(tx);
|
|
@@ -61,7 +61,7 @@ class UserService {
|
|
|
61
61
|
findByClerkUserId(clerkUserId, tx) {
|
|
62
62
|
return __awaiter(this, void 0, void 0, function* () {
|
|
63
63
|
const client = checkAndFallbackWithNonTCClient(tx);
|
|
64
|
-
// DB
|
|
64
|
+
// Partial DB indexes match this status filter, so findUnique is valid here.
|
|
65
65
|
return yield client.user.findUnique({
|
|
66
66
|
where: {
|
|
67
67
|
clerkUserId,
|
|
@@ -135,7 +135,7 @@ class UserService {
|
|
|
135
135
|
return { users, total };
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
-
//
|
|
138
|
+
// Create anonymous users in bulk
|
|
139
139
|
createBatchAnonymousUsers(fingerprintIds, tx) {
|
|
140
140
|
return __awaiter(this, void 0, void 0, function* () {
|
|
141
141
|
const client = checkAndFallbackWithNonTCClient(tx);
|
|
@@ -236,7 +236,7 @@ function handleInvoicePaid(invoice) {
|
|
|
236
236
|
periodEnd: utils.viewLocalTime(subPeriodEnd),
|
|
237
237
|
});
|
|
238
238
|
if (isInitialPayment) {
|
|
239
|
-
//
|
|
239
|
+
// check
|
|
240
240
|
const nonActiveSubscription = yield subscription_service.subscriptionService.getNonActiveSubscription(userId);
|
|
241
241
|
if (!nonActiveSubscription) {
|
|
242
242
|
throw new Error(`Subscription status is ACTIVE for user ${userId}, forbidden to re-active!`);
|
|
@@ -263,7 +263,7 @@ function handleInvoicePaid(invoice) {
|
|
|
263
263
|
return;
|
|
264
264
|
}
|
|
265
265
|
if (isRenewal) {
|
|
266
|
-
//
|
|
266
|
+
// must query it's subscription db record
|
|
267
267
|
const subscription = yield subscription_service.subscriptionService.findByPaySubscriptionId(subscriptionId);
|
|
268
268
|
if (!subscription) {
|
|
269
269
|
throw new Error(`Subscription not found for renewal: ${subscriptionId}`);
|
|
@@ -274,8 +274,8 @@ function handleInvoicePaid(invoice) {
|
|
|
274
274
|
throw new Error(`Renewal invoice ${invoice.id} already processed as ${existingOrder.orderId}, skipping.`);
|
|
275
275
|
}
|
|
276
276
|
// Get credits from current price configuration (handles plan upgrades/downgrades)
|
|
277
|
-
//
|
|
278
|
-
//
|
|
277
|
+
// Prefer values from configuration; fall back to the previous cycle value if unavailable. Manual remediation will be applied later if issues occur, prioritizing functional availability
|
|
278
|
+
// No error occurs here as long as the configuration is correct!
|
|
279
279
|
const creditsForRenewal = subscription.priceId
|
|
280
280
|
? moneyPriceConfig.getCreditsFromPriceId(subscription.priceId)
|
|
281
281
|
: subscription.creditsAllocated;
|
|
@@ -386,7 +386,7 @@ function handleInvoicePaymentFailed(invoice) {
|
|
|
386
386
|
}
|
|
387
387
|
const subscriptionId = parentDetails.subscription;
|
|
388
388
|
const subscriptionMetadata = parentDetails.metadata || {};
|
|
389
|
-
//
|
|
389
|
+
// PaymentIntentId
|
|
390
390
|
const paymentIntentId = yield stripeConfig.fetchPaymentId(invoice.id);
|
|
391
391
|
console.log('Invoice payment failed event key-info:', {
|
|
392
392
|
invoiceId: invoice.id,
|
|
@@ -234,7 +234,7 @@ function handleInvoicePaid(invoice) {
|
|
|
234
234
|
periodEnd: viewLocalTime(subPeriodEnd),
|
|
235
235
|
});
|
|
236
236
|
if (isInitialPayment) {
|
|
237
|
-
//
|
|
237
|
+
// check
|
|
238
238
|
const nonActiveSubscription = yield subscriptionService.getNonActiveSubscription(userId);
|
|
239
239
|
if (!nonActiveSubscription) {
|
|
240
240
|
throw new Error(`Subscription status is ACTIVE for user ${userId}, forbidden to re-active!`);
|
|
@@ -261,7 +261,7 @@ function handleInvoicePaid(invoice) {
|
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
if (isRenewal) {
|
|
264
|
-
//
|
|
264
|
+
// must query it's subscription db record
|
|
265
265
|
const subscription = yield subscriptionService.findByPaySubscriptionId(subscriptionId);
|
|
266
266
|
if (!subscription) {
|
|
267
267
|
throw new Error(`Subscription not found for renewal: ${subscriptionId}`);
|
|
@@ -272,8 +272,8 @@ function handleInvoicePaid(invoice) {
|
|
|
272
272
|
throw new Error(`Renewal invoice ${invoice.id} already processed as ${existingOrder.orderId}, skipping.`);
|
|
273
273
|
}
|
|
274
274
|
// Get credits from current price configuration (handles plan upgrades/downgrades)
|
|
275
|
-
//
|
|
276
|
-
//
|
|
275
|
+
// Prefer values from configuration; fall back to the previous cycle value if unavailable. Manual remediation will be applied later if issues occur, prioritizing functional availability
|
|
276
|
+
// No error occurs here as long as the configuration is correct!
|
|
277
277
|
const creditsForRenewal = subscription.priceId
|
|
278
278
|
? getCreditsFromPriceId(subscription.priceId)
|
|
279
279
|
: subscription.creditsAllocated;
|
|
@@ -384,7 +384,7 @@ function handleInvoicePaymentFailed(invoice) {
|
|
|
384
384
|
}
|
|
385
385
|
const subscriptionId = parentDetails.subscription;
|
|
386
386
|
const subscriptionMetadata = parentDetails.metadata || {};
|
|
387
|
-
//
|
|
387
|
+
// PaymentIntentId
|
|
388
388
|
const paymentIntentId = yield fetchPaymentId(invoice.id);
|
|
389
389
|
console.log('Invoice payment failed event key-info:', {
|
|
390
390
|
invoiceId: invoice.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windrun-huaiin/backend-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "31.0.0",
|
|
4
4
|
"description": "Shared backend primitives: Prisma schema/client, database services, routing helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"LICENSE"
|
|
140
140
|
],
|
|
141
141
|
"dependencies": {
|
|
142
|
-
"@clerk/nextjs": "^7.
|
|
142
|
+
"@clerk/nextjs": "^7.3.3",
|
|
143
143
|
"@upstash/redis": "^1.34.0",
|
|
144
144
|
"@upstash/qstash": "^2.7.0",
|
|
145
145
|
"@upstash/lock": "^0.2.1",
|
|
@@ -148,9 +148,9 @@
|
|
|
148
148
|
"svix": "^1.86.0",
|
|
149
149
|
"tslib": "^2.8.1",
|
|
150
150
|
"zod": "^4.3.6",
|
|
151
|
-
"@windrun-huaiin/
|
|
152
|
-
"@windrun-huaiin/
|
|
153
|
-
"@windrun-huaiin/
|
|
151
|
+
"@windrun-huaiin/lib": "^31.0.0",
|
|
152
|
+
"@windrun-huaiin/third-ui": "^31.0.0",
|
|
153
|
+
"@windrun-huaiin/contracts": "^31.0.0"
|
|
154
154
|
},
|
|
155
155
|
"devDependencies": {
|
|
156
156
|
"@rollup/plugin-alias": "^5.1.1",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
},
|
|
164
164
|
"peerDependencies": {
|
|
165
165
|
"@windrun-huaiin/contracts": ">=22.0.0",
|
|
166
|
-
"@clerk/nextjs": "^7.
|
|
166
|
+
"@clerk/nextjs": "^7.3.3",
|
|
167
167
|
"@prisma/client": "^7.8.0",
|
|
168
168
|
"next": "16.1.6",
|
|
169
169
|
"stripe": "22.0.2",
|
|
@@ -172,6 +172,13 @@
|
|
|
172
172
|
"publishConfig": {
|
|
173
173
|
"access": "public"
|
|
174
174
|
},
|
|
175
|
+
"author": "windrun-huaiin",
|
|
176
|
+
"homepage": "https://d8ger.com",
|
|
177
|
+
"repository": {
|
|
178
|
+
"type": "git",
|
|
179
|
+
"url": "git+https://github.com/caofanCPU/next-ai-build.git",
|
|
180
|
+
"directory": "packages/backend-core"
|
|
181
|
+
},
|
|
175
182
|
"scripts": {
|
|
176
183
|
"build": "rm -rf dist && rollup -c rollup.config.mjs",
|
|
177
184
|
"build:prod": "rm -rf dist && rollup -c rollup.config.mjs",
|
|
@@ -23,9 +23,9 @@ import { finalizeUserContext } from '@core/context/user-context-finalizer';
|
|
|
23
23
|
import type { CoreJsonObject } from '@core/db/prisma-model-type';
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
// ====================
|
|
26
|
+
// ==================== Type Definitions ====================
|
|
27
27
|
|
|
28
|
-
/**
|
|
28
|
+
/** Successful response payload */
|
|
29
29
|
interface XUserResponse {
|
|
30
30
|
success: true;
|
|
31
31
|
xUser: XUser;
|
|
@@ -36,14 +36,14 @@ interface XUserResponse {
|
|
|
36
36
|
hasAnonymousUser?: boolean;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
39
|
+
/** Error response payload */
|
|
40
40
|
interface ErrorResponse {
|
|
41
41
|
error: string;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// ====================
|
|
44
|
+
// ==================== Utilities ====================
|
|
45
45
|
|
|
46
|
-
/**
|
|
46
|
+
/** Create a successful response payload */
|
|
47
47
|
function createSuccessResponse(params: {
|
|
48
48
|
entities: UserContextEntities;
|
|
49
49
|
isNewUser: boolean;
|
|
@@ -64,7 +64,7 @@ function createSuccessResponse(params: {
|
|
|
64
64
|
return finalizeUserContext(response);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/** Create an error response */
|
|
68
68
|
function createErrorResponse(message: string, status = 400): NextResponse {
|
|
69
69
|
const errorResponse: ErrorResponse = { error: message };
|
|
70
70
|
return NextResponse.json(errorResponse, { status });
|
|
@@ -552,7 +552,7 @@ function finalizeAttribution(sourceRef: SourceRefData) {
|
|
|
552
552
|
sourceRef.sourceType = 'direct';
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
-
//
|
|
555
|
+
// Extract the user's first-touch attribution source.
|
|
556
556
|
function extractSourceRef(request: NextRequest): SourceRefData | null {
|
|
557
557
|
const headerRef = request.headers.get('referer') || request.headers.get('referrer');
|
|
558
558
|
const customRef = request.headers.get('x-source-ref');
|
|
@@ -603,7 +603,7 @@ function extractSourceRef(request: NextRequest): SourceRefData | null {
|
|
|
603
603
|
|
|
604
604
|
|
|
605
605
|
/**
|
|
606
|
-
*
|
|
606
|
+
* Query the user by Clerk user ID and return response data.
|
|
607
607
|
*/
|
|
608
608
|
async function getUserByClerkId(clerkUserId: string): Promise<XUserResponse | null> {
|
|
609
609
|
const entities = await fetchUserContextByClerkUserId(clerkUserId);
|
|
@@ -618,7 +618,7 @@ async function getUserByClerkId(clerkUserId: string): Promise<XUserResponse | nu
|
|
|
618
618
|
}
|
|
619
619
|
|
|
620
620
|
/**
|
|
621
|
-
*
|
|
621
|
+
* Query the user by fingerprint ID and return response data.
|
|
622
622
|
*/
|
|
623
623
|
async function getUserByFingerprintId(fingerprintId: string): Promise<XUserResponse | null> {
|
|
624
624
|
const result = await fetchLatestUserContextByFingerprintId(fingerprintId);
|
|
@@ -639,12 +639,12 @@ async function getUserByFingerprintId(fingerprintId: string): Promise<XUserRespo
|
|
|
639
639
|
}
|
|
640
640
|
|
|
641
641
|
/**
|
|
642
|
-
*
|
|
642
|
+
* Shared fingerprint request handling logic.
|
|
643
643
|
*/
|
|
644
644
|
async function handleFingerprintRequest(request: NextRequest, options: { createIfNotExists?: boolean; } = {}) {
|
|
645
|
-
//
|
|
645
|
+
// Extract the fingerprint ID from the request.
|
|
646
646
|
const fingerprintId = extractFingerprintFromNextRequest(request);
|
|
647
|
-
//
|
|
647
|
+
// Validate the fingerprint ID.
|
|
648
648
|
if (!fingerprintId) {
|
|
649
649
|
return createErrorResponse('Invalid or missing fingerprint ID');
|
|
650
650
|
}
|
|
@@ -653,27 +653,26 @@ async function handleFingerprintRequest(request: NextRequest, options: { createI
|
|
|
653
653
|
const authIdentity = await getOptionalServerAuthIdentity();
|
|
654
654
|
const clerkUserId = authIdentity?.providerUserId ?? null;
|
|
655
655
|
try {
|
|
656
|
-
//
|
|
656
|
+
// Prefer Clerk user ID lookup when the user is authenticated.
|
|
657
657
|
let existingUserResult: XUserResponse | null = null;
|
|
658
658
|
if (clerkUserId) {
|
|
659
|
-
//
|
|
659
|
+
// Authenticated users are always resolved by clerkUserId.
|
|
660
660
|
existingUserResult = await getUserByClerkId(clerkUserId);
|
|
661
661
|
if (existingUserResult && existingUserResult.xUser.fingerprintId !== fingerprintId) {
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
// 就是以当前登录用户去查他自己的数据就行!
|
|
662
|
+
// The authenticated user's fingerprint changed. Clerk still identifies the account as the same user.
|
|
663
|
+
// Trust clerkUserId as the source of truth and keep resolving the user's own data by login identity.
|
|
664
|
+
// A single fingerprint can be associated with multiple accounts, so no mutation is needed here.
|
|
666
665
|
console.warn(`Current login user used diff fp_ids: ${clerkUserId}, db_fp_id=${existingUserResult.xUser.fingerprintId}, req_fp_id=${fingerprintId}`);
|
|
667
666
|
}
|
|
668
667
|
} else {
|
|
669
|
-
//
|
|
668
|
+
// For anonymous requests, fall back to fingerprint lookup.
|
|
670
669
|
existingUserResult = await getUserByFingerprintId(fingerprintId);
|
|
671
670
|
}
|
|
672
671
|
if (existingUserResult) {
|
|
673
672
|
return NextResponse.json(existingUserResult);
|
|
674
673
|
}
|
|
675
674
|
|
|
676
|
-
//
|
|
675
|
+
// If the user does not exist and creation is disabled, return 404.
|
|
677
676
|
if (!options.createIfNotExists) {
|
|
678
677
|
return createErrorResponse('User not found', 404);
|
|
679
678
|
}
|
|
@@ -689,7 +688,7 @@ async function handleFingerprintRequest(request: NextRequest, options: { createI
|
|
|
689
688
|
console.log(`Created new anonymous user ${anonymousInitResult.user.userId} with fingerprint ${fingerprintId}`);
|
|
690
689
|
}
|
|
691
690
|
|
|
692
|
-
//
|
|
691
|
+
// Return the created or existing context.
|
|
693
692
|
const response = createSuccessResponse({
|
|
694
693
|
entities: {
|
|
695
694
|
user: anonymousInitResult.user,
|
|
@@ -711,7 +710,7 @@ async function handleFingerprintRequest(request: NextRequest, options: { createI
|
|
|
711
710
|
}
|
|
712
711
|
|
|
713
712
|
/**
|
|
714
|
-
*
|
|
713
|
+
* Anonymous user initialization API.
|
|
715
714
|
* POST /api/user/anonymous/init
|
|
716
715
|
*/
|
|
717
716
|
export async function POST(request: NextRequest) {
|
|
@@ -11,7 +11,7 @@ import { headers } from 'next/headers';
|
|
|
11
11
|
import { NextRequest, NextResponse } from 'next/server';
|
|
12
12
|
import { Webhook } from 'svix';
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Clerk webhook event type definition
|
|
15
15
|
interface ClerkWebhookEvent {
|
|
16
16
|
data: {
|
|
17
17
|
id: string;
|
|
@@ -41,12 +41,12 @@ interface ClerkWebhookEvent {
|
|
|
41
41
|
}
|
|
42
42
|
export async function POST(request: NextRequest) {
|
|
43
43
|
try {
|
|
44
|
-
//
|
|
44
|
+
// Read the raw request body.
|
|
45
45
|
const rawBody = await request.text();
|
|
46
46
|
|
|
47
47
|
let event: ClerkWebhookEvent;
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// Skip signature verification in development.
|
|
50
50
|
if (process.env.NODE_ENV === 'development') {
|
|
51
51
|
console.log('Development mode: skipping webhook signature verification');
|
|
52
52
|
try {
|
|
@@ -59,13 +59,13 @@ export async function POST(request: NextRequest) {
|
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
} else {
|
|
62
|
-
//
|
|
62
|
+
// Verify the signature in production.
|
|
63
63
|
const headerPayload = await headers();
|
|
64
64
|
const svix_id = headerPayload.get('svix-id');
|
|
65
65
|
const svix_timestamp = headerPayload.get('svix-timestamp');
|
|
66
66
|
const svix_signature = headerPayload.get('svix-signature');
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// Reject requests missing required headers.
|
|
69
69
|
if (!svix_id || !svix_timestamp || !svix_signature) {
|
|
70
70
|
return NextResponse.json(
|
|
71
71
|
{ error: 'Missing webhook headers' },
|
|
@@ -73,7 +73,7 @@ export async function POST(request: NextRequest) {
|
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
//
|
|
76
|
+
// Load the webhook signing secret.
|
|
77
77
|
const webhookSecret = process.env.CLERK_WEBHOOK_SECRET;
|
|
78
78
|
if (!webhookSecret) {
|
|
79
79
|
console.error('CLERK_WEBHOOK_SECRET is not configured');
|
|
@@ -83,7 +83,7 @@ export async function POST(request: NextRequest) {
|
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
86
|
+
// Verify the webhook signature.
|
|
87
87
|
try {
|
|
88
88
|
const wh = new Webhook(webhookSecret);
|
|
89
89
|
event = wh.verify(rawBody, {
|
|
@@ -110,7 +110,7 @@ export async function POST(request: NextRequest) {
|
|
|
110
110
|
let processingResult = { success: true, message: 'Event processed successfully' };
|
|
111
111
|
|
|
112
112
|
try {
|
|
113
|
-
//
|
|
113
|
+
// Dispatch by event type.
|
|
114
114
|
const { type } = event;
|
|
115
115
|
|
|
116
116
|
switch (type) {
|
|
@@ -155,7 +155,7 @@ export async function POST(request: NextRequest) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
*
|
|
158
|
+
* Handle the user.created event.
|
|
159
159
|
*/
|
|
160
160
|
async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
161
161
|
const { data } = event;
|
|
@@ -174,7 +174,7 @@ async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
|
174
174
|
userName
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
//
|
|
177
|
+
// Validate required fields.
|
|
178
178
|
if (!fingerprintId) {
|
|
179
179
|
console.error('Missing fingerprintId in webhook data, process flow error');
|
|
180
180
|
return;
|
|
@@ -186,17 +186,17 @@ async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
try {
|
|
189
|
-
//
|
|
189
|
+
// Find all non-deleted users for this device fingerprint.
|
|
190
190
|
const existingUsers = await userService.findListByFingerprintId(fingerprintId);
|
|
191
191
|
if (!existingUsers || existingUsers.length === 0) {
|
|
192
192
|
console.error('Invalid fingerprintId in webhook data, process flow error');
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
//
|
|
196
|
+
// Find an existing user with the same email.
|
|
197
197
|
const sameEmailUser = existingUsers.find(user => user.email === email);
|
|
198
198
|
if (sameEmailUser) {
|
|
199
|
-
//
|
|
199
|
+
// Same account; update clerkUserId if needed.
|
|
200
200
|
if (sameEmailUser.clerkUserId !== clerkUserId) {
|
|
201
201
|
await userService.updateUser(sameEmailUser.userId, { clerkUserId, userName: userName, status: UserStatus.REGISTERED });
|
|
202
202
|
console.log(`Updated clerkUserId for user ${sameEmailUser.userId}`);
|
|
@@ -206,16 +206,16 @@ async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
//
|
|
209
|
+
// Find an anonymous user with no email or clerkUserId.
|
|
210
210
|
const anonymousUser = existingUsers.find(user => !user.email && !user.clerkUserId && user.status === UserStatus.ANONYMOUS );
|
|
211
211
|
if (anonymousUser) {
|
|
212
|
-
//
|
|
212
|
+
// Upgrade the anonymous user.
|
|
213
213
|
await userAggregateService.upgradeToRegistered(anonymousUser.userId, email, clerkUserId, userName);
|
|
214
214
|
console.log(`Successfully upgraded anonymous user ${anonymousUser.userId} to registered user`);
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
//
|
|
218
|
+
// New account on the same device; create a new user.
|
|
219
219
|
await userAggregateService.createNewRegisteredUser(clerkUserId, email, fingerprintId, userName);
|
|
220
220
|
console.log(`Created new user for device ${fingerprintId} with email ${email}`);
|
|
221
221
|
|
|
@@ -226,7 +226,7 @@ async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
|
-
*
|
|
229
|
+
* Handle the user.deleted event.
|
|
230
230
|
*/
|
|
231
231
|
async function handleUserDeleted(event: ClerkWebhookEvent) {
|
|
232
232
|
const { data } = event;
|
package/src/auth/auth-utils.ts
CHANGED
|
@@ -4,9 +4,6 @@ import { userService } from '../services/database/index';
|
|
|
4
4
|
import { User } from '../services/database/prisma-model-type';
|
|
5
5
|
import { AUTH_ERRORS, AUTH_HEADERS, type AuthProvider, type ProviderIdentity } from './auth-shared';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* 认证结果类型
|
|
9
|
-
*/
|
|
10
7
|
export interface AuthResult {
|
|
11
8
|
userId: string;
|
|
12
9
|
user: User;
|
|
@@ -15,7 +12,7 @@ export interface AuthResult {
|
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
|
-
*
|
|
15
|
+
* Fetch User's info from header field by Middleware
|
|
19
16
|
*/
|
|
20
17
|
export async function getAuthenticatedUser(req: NextRequest): Promise<AuthResult> {
|
|
21
18
|
try {
|
|
@@ -43,7 +40,7 @@ export async function getAuthenticatedUser(req: NextRequest): Promise<AuthResult
|
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
/**
|
|
46
|
-
*
|
|
43
|
+
* Require Auth, success back user's id
|
|
47
44
|
*/
|
|
48
45
|
export async function requireAuth(req: NextRequest): Promise<string> {
|
|
49
46
|
const auth = await getAuthenticatedUser(req);
|
|
@@ -51,15 +48,15 @@ export async function requireAuth(req: NextRequest): Promise<string> {
|
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
/**
|
|
54
|
-
*
|
|
51
|
+
* Require Auth, success back user's info
|
|
55
52
|
*/
|
|
56
53
|
export async function requireAuthWithUser(req: NextRequest): Promise<AuthResult> {
|
|
57
54
|
return await getAuthenticatedUser(req);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
58
|
+
* Only use in server side
|
|
59
|
+
* Server Component / Server Action, just need user's login status
|
|
63
60
|
*/
|
|
64
61
|
export async function getOptionalServerAuthIdentity(): Promise<ProviderIdentity | null> {
|
|
65
62
|
try {
|
|
@@ -79,8 +76,8 @@ export async function getOptionalServerAuthIdentity(): Promise<ProviderIdentity
|
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
79
|
+
* Only use in server side
|
|
80
|
+
* Server Component / Server Action, need user's login status and user's data, will check db
|
|
84
81
|
*/
|
|
85
82
|
export async function getOptionalServerAuthUser(): Promise<AuthResult | null> {
|
|
86
83
|
try {
|
|
@@ -107,7 +104,7 @@ export async function getOptionalServerAuthUser(): Promise<AuthResult | null> {
|
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
/**
|
|
110
|
-
* API Route
|
|
107
|
+
* API Route Auth Util
|
|
111
108
|
*/
|
|
112
109
|
export class ApiAuthUtils {
|
|
113
110
|
private req: NextRequest;
|
|
@@ -116,23 +113,14 @@ export class ApiAuthUtils {
|
|
|
116
113
|
this.req = req;
|
|
117
114
|
}
|
|
118
115
|
|
|
119
|
-
/**
|
|
120
|
-
* 要求用户必须已认证,返回用户ID
|
|
121
|
-
*/
|
|
122
116
|
async requireAuth(): Promise<string> {
|
|
123
117
|
return await requireAuth(this.req);
|
|
124
118
|
}
|
|
125
119
|
|
|
126
|
-
/**
|
|
127
|
-
* 要求用户必须已认证,返回完整用户信息
|
|
128
|
-
*/
|
|
129
120
|
async requireAuthWithUser(): Promise<AuthResult> {
|
|
130
121
|
return await requireAuthWithUser(this.req);
|
|
131
122
|
}
|
|
132
123
|
|
|
133
|
-
/**
|
|
134
|
-
* 获取用户ID(如果已认证)
|
|
135
|
-
*/
|
|
136
124
|
async getUserId(): Promise<string | null> {
|
|
137
125
|
try {
|
|
138
126
|
const auth = await getAuthenticatedUser(this.req);
|
|
@@ -142,9 +130,6 @@ export class ApiAuthUtils {
|
|
|
142
130
|
}
|
|
143
131
|
}
|
|
144
132
|
|
|
145
|
-
/**
|
|
146
|
-
* 获取完整用户信息(如果已认证)
|
|
147
|
-
*/
|
|
148
133
|
async getUser(): Promise<AuthResult | null> {
|
|
149
134
|
try {
|
|
150
135
|
return await getAuthenticatedUser(this.req);
|
|
@@ -12,7 +12,7 @@ export const moneyPriceConfig: MoneyPriceConfig = {
|
|
|
12
12
|
stripe: {
|
|
13
13
|
provider: 'stripe',
|
|
14
14
|
enabled: true,
|
|
15
|
-
//
|
|
15
|
+
// Subscription products
|
|
16
16
|
subscriptionProducts: {
|
|
17
17
|
F1: {
|
|
18
18
|
key: 'F1',
|
|
@@ -70,7 +70,7 @@ export const moneyPriceConfig: MoneyPriceConfig = {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
-
//
|
|
73
|
+
// Credit pack products
|
|
74
74
|
creditPackProducts: {
|
|
75
75
|
F1: {
|
|
76
76
|
key: 'F1',
|
|
@@ -106,56 +106,56 @@ export const moneyPriceConfig: MoneyPriceConfig = {
|
|
|
106
106
|
}
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
// ============
|
|
109
|
+
// ============ Application-level wrappers that hide moneyPriceConfig details ============
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
112
|
+
* Get the currently active payment provider configuration.
|
|
113
113
|
*
|
|
114
|
-
*
|
|
115
|
-
* -
|
|
116
|
-
* -
|
|
117
|
-
* -
|
|
114
|
+
* Security design:
|
|
115
|
+
* - Wrapper functions keep moneyPriceConfig private.
|
|
116
|
+
* - Utility functions extract the active provider configuration from the config.
|
|
117
|
+
* - External callers can access only this wrapper, not the full config object.
|
|
118
118
|
*
|
|
119
|
-
* @returns
|
|
119
|
+
* @returns The currently active payment provider configuration.
|
|
120
120
|
*/
|
|
121
121
|
export function getActiveProviderConfig(): PaymentProviderConfig {
|
|
122
122
|
return getActiveProviderConfigUtil(moneyPriceConfig);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
126
|
+
* Get the credit amount for a price ID.
|
|
127
127
|
*
|
|
128
|
-
*
|
|
129
|
-
* -
|
|
130
|
-
* -
|
|
131
|
-
* -
|
|
128
|
+
* Security design:
|
|
129
|
+
* - Wrapper functions keep moneyPriceConfig private.
|
|
130
|
+
* - Utility functions parse the config and extract the result.
|
|
131
|
+
* - External callers can access only this wrapper, not the full config object.
|
|
132
132
|
*
|
|
133
|
-
* @param priceId -
|
|
134
|
-
* @param _provider -
|
|
135
|
-
* @returns
|
|
133
|
+
* @param priceId - Price ID to query.
|
|
134
|
+
* @param _provider - Reserved for backward compatibility; currently unused.
|
|
135
|
+
* @returns The matching credit amount, or null.
|
|
136
136
|
*/
|
|
137
137
|
export function getCreditsFromPriceId(priceId?: string, _provider?: string): number | null {
|
|
138
138
|
return getCreditsFromPriceIdUtil(priceId, moneyPriceConfig);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
*
|
|
142
|
+
* Get price configuration by query parameters.
|
|
143
143
|
*
|
|
144
|
-
*
|
|
145
|
-
* 1.
|
|
146
|
-
* 2.
|
|
147
|
-
* 3.
|
|
144
|
+
* Supported query modes:
|
|
145
|
+
* 1. By priceId: getPriceConfig(priceId='price_xxx')
|
|
146
|
+
* 2. By plan and billingType: getPriceConfig(undefined, 'P2', 'monthly')
|
|
147
|
+
* 3. By plan: getPriceConfig(undefined, 'P2')
|
|
148
148
|
*
|
|
149
|
-
*
|
|
150
|
-
* -
|
|
151
|
-
* -
|
|
152
|
-
* -
|
|
149
|
+
* Security design:
|
|
150
|
+
* - Wrapper functions keep moneyPriceConfig private.
|
|
151
|
+
* - Utility functions parse the config and extract the matching result.
|
|
152
|
+
* - External callers can access only this wrapper, not the full config object.
|
|
153
153
|
*
|
|
154
|
-
* @param priceId -
|
|
155
|
-
* @param plan -
|
|
156
|
-
* @param billingType -
|
|
157
|
-
* @param _provider -
|
|
158
|
-
* @returns
|
|
154
|
+
* @param priceId - Optional price ID to query.
|
|
155
|
+
* @param plan - Optional plan name, such as 'P2' or 'U3'.
|
|
156
|
+
* @param billingType - Optional billing type, such as 'monthly' or 'yearly'.
|
|
157
|
+
* @param _provider - Reserved for backward compatibility; currently unused.
|
|
158
|
+
* @returns The matching price config with derived metadata: priceName, description, and interval.
|
|
159
159
|
*/
|
|
160
160
|
export function getPriceConfig(
|
|
161
161
|
priceId?: string,
|
|
@@ -165,4 +165,3 @@ export function getPriceConfig(
|
|
|
165
165
|
): (EnhancePricePlan & { priceName: string; description: string; interval?: string }) | null {
|
|
166
166
|
return getPriceConfigUtil(priceId, plan, billingType, moneyPriceConfig);
|
|
167
167
|
}
|
|
168
|
-
|