Compare commits
5 Commits
cloudflare
...
dev/fix-we
Author | SHA1 | Date | |
---|---|---|---|
|
5136f64252 | ||
|
b8d3d09d9e | ||
|
5cebf2ef00 | ||
|
787548a269 | ||
|
aea55ee4bb |
7483
cloudflare-env.d.ts
vendored
7483
cloudflare-env.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,14 @@ After that, you can return to the blog post and you can read the rest of the blo
|
||||
|
||||
For more details, please check out the documentation: [Blog](https://mksaas.com/docs/blog).
|
||||
|
||||
Test show Tweet in the blog post.
|
||||
|
||||
<XEmbedClient url="https://x.com/mksaascom/status/1960417768505008291" width={500} />
|
||||
|
||||
Test show YouTube video in the blog post.
|
||||
|
||||
<YoutubeVideo url="https://www.youtube.com/embed/xvoeSnlFZJk" width={500} />
|
||||
|
||||
Now the rest of the blog post is premium content.
|
||||
|
||||
<PremiumContent>
|
||||
|
@ -35,6 +35,14 @@ CVV: 567
|
||||
|
||||
更多详情,请参考文档:[博客](https://mksaas.com/docs/blog)。
|
||||
|
||||
测试展示 X 帖子。
|
||||
|
||||
<XEmbedClient url="https://x.com/mksaascom/status/1960417768505008291" width={500} />
|
||||
|
||||
测试展示 YouTube 视频。
|
||||
|
||||
<YoutubeVideo url="https://www.youtube.com/embed/xvoeSnlFZJk" width={500} />
|
||||
|
||||
现在剩下的内容是付费内容。
|
||||
|
||||
<PremiumContent>
|
||||
|
@ -1 +0,0 @@
|
||||
NEXTJS_ENV=development
|
@ -18,18 +18,6 @@ const nextConfig: NextConfig = {
|
||||
// removeConsole: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
|
||||
// https://github.com/vercel/next.js/discussions/50177#discussioncomment-6006702
|
||||
// fix build error: Module build failed: UnhandledSchemeError:
|
||||
// Reading from "cloudflare:sockets" is not handled by plugins (Unhandled scheme).
|
||||
webpack: (config, { webpack }) => {
|
||||
config.plugins.push(
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /^pg-native$|^cloudflare:sockets$/,
|
||||
})
|
||||
);
|
||||
return config;
|
||||
},
|
||||
|
||||
images: {
|
||||
// https://vercel.com/docs/image-optimization/managing-image-optimization-costs#minimizing-image-optimization-costs
|
||||
// https://nextjs.org/docs/app/api-reference/components/image#unoptimized
|
||||
@ -82,9 +70,3 @@ const withNextIntl = createNextIntlPlugin();
|
||||
const withMDX = createMDX();
|
||||
|
||||
export default withMDX(withNextIntl(nextConfig));
|
||||
|
||||
// https://opennext.js.org/cloudflare/get-started#12-develop-locally
|
||||
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||
|
||||
// during local development, to access in any of your server code, local versions of Cloudflare bindings
|
||||
initOpenNextCloudflareForDev();
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
||||
|
||||
|
||||
export default defineCloudflareConfig({
|
||||
|
||||
});
|
@ -4,7 +4,6 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"cf-dev": "next dev -p 8787",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
@ -112,7 +111,6 @@
|
||||
"next-intl": "^4.0.0",
|
||||
"next-safe-action": "^7.10.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"pg": "^8.16.0",
|
||||
"nuqs": "^2.5.1",
|
||||
"postgres": "^3.4.5",
|
||||
"radix-ui": "^1.4.2",
|
||||
@ -122,6 +120,7 @@
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-remove-scroll": "^2.6.3",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-social-media-embed": "^2.5.18",
|
||||
"react-syntax-highlighter": "^15.6.3",
|
||||
"react-tweet": "^3.2.2",
|
||||
"react-use-measure": "^2.1.7",
|
||||
@ -145,7 +144,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@opennextjs/cloudflare": "^1.6.5",
|
||||
"@tailwindcss/postcss": "^4.0.14",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@types/mdx": "^2.0.13",
|
||||
@ -160,7 +158,6 @@
|
||||
"react-email": "3.0.7",
|
||||
"tailwindcss": "^4.0.14",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.28.1"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
4394
pnpm-lock.yaml
generated
4394
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,2 +0,0 @@
|
||||
/_next/static/*
|
||||
Cache-Control: public,max-age=31536000,immutable
|
@ -12,6 +12,7 @@ import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import { XEmbedClient } from './xembed';
|
||||
|
||||
/**
|
||||
* Enhanced MDX Content component that includes commonly used MDX components
|
||||
@ -23,6 +24,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
...defaultMdxComponents,
|
||||
...LucideIcons,
|
||||
// ...((await import('lucide-react')) as unknown as MDXComponents),
|
||||
XEmbedClient,
|
||||
YoutubeVideo,
|
||||
PremiumContent,
|
||||
Tabs,
|
||||
|
16
src/components/docs/xembed.tsx
Normal file
16
src/components/docs/xembed.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { XEmbed, type XEmbedProps } from 'react-social-media-embed';
|
||||
|
||||
/**
|
||||
* Embedding X Posts in Fumadocs
|
||||
*
|
||||
* https://rjv.im/blog/solution/embed-x-post-in-fuma-docs
|
||||
*/
|
||||
export function XEmbedClient({ ...props }: XEmbedProps) {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<XEmbed {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -2,23 +2,17 @@
|
||||
* Connect to PostgreSQL Database (Supabase/Neon/Local PostgreSQL)
|
||||
* https://orm.drizzle.team/docs/tutorials/drizzle-with-supabase
|
||||
*/
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema';
|
||||
|
||||
let db: ReturnType<typeof drizzle> | null = null;
|
||||
|
||||
// https://opennext.js.org/cloudflare/howtos/db#postgresql
|
||||
export async function getDb() {
|
||||
if (db) return db;
|
||||
const { env } = await getCloudflareContext({ async: true });
|
||||
const pool = new Pool({
|
||||
connectionString: env.HYPERDRIVE.connectionString,
|
||||
// You don't want to reuse the same connection for multiple requests
|
||||
maxUses: 1,
|
||||
});
|
||||
db = drizzle({ client: pool, schema });
|
||||
const connectionString = process.env.DATABASE_URL!;
|
||||
const client = postgres(connectionString, { prepare: false });
|
||||
db = drizzle(client, { schema });
|
||||
return db;
|
||||
}
|
||||
|
||||
|
3
src/db/migrations/0004_clever_molly_hayes.sql
Normal file
3
src/db/migrations/0004_clever_molly_hayes.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE "payment" ADD COLUMN "invoice_id" text;--> statement-breakpoint
|
||||
CREATE INDEX "payment_invoice_id_idx" ON "payment" USING btree ("invoice_id");--> statement-breakpoint
|
||||
ALTER TABLE "payment" ADD CONSTRAINT "payment_invoice_id_unique" UNIQUE("invoice_id");
|
946
src/db/migrations/meta/0004_snapshot.json
Normal file
946
src/db/migrations/meta/0004_snapshot.json
Normal file
@ -0,0 +1,946 @@
|
||||
{
|
||||
"id": "adff59d1-8ceb-4472-ae47-148a950e700a",
|
||||
"prevId": "318baf42-b0f6-4288-807b-778767149685",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_user_id_idx": {
|
||||
"name": "account_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"account_account_id_idx": {
|
||||
"name": "account_account_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "account_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"account_provider_id_idx": {
|
||||
"name": "account_provider_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "provider_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.credit_transaction": {
|
||||
"name": "credit_transaction",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"remaining_amount": {
|
||||
"name": "remaining_amount",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"expiration_date": {
|
||||
"name": "expiration_date",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"expiration_date_processed_at": {
|
||||
"name": "expiration_date_processed_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"credit_transaction_user_id_idx": {
|
||||
"name": "credit_transaction_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"credit_transaction_type_idx": {
|
||||
"name": "credit_transaction_type_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "type",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"credit_transaction_user_id_user_id_fk": {
|
||||
"name": "credit_transaction_user_id_user_id_fk",
|
||||
"tableFrom": "credit_transaction",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.payment": {
|
||||
"name": "payment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"price_id": {
|
||||
"name": "price_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"interval": {
|
||||
"name": "interval",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"subscription_id": {
|
||||
"name": "subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"session_id": {
|
||||
"name": "session_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"invoice_id": {
|
||||
"name": "invoice_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"period_start": {
|
||||
"name": "period_start",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"period_end": {
|
||||
"name": "period_end",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cancel_at_period_end": {
|
||||
"name": "cancel_at_period_end",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trial_start": {
|
||||
"name": "trial_start",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trial_end": {
|
||||
"name": "trial_end",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"payment_type_idx": {
|
||||
"name": "payment_type_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "type",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"payment_price_id_idx": {
|
||||
"name": "payment_price_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "price_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"payment_user_id_idx": {
|
||||
"name": "payment_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"payment_customer_id_idx": {
|
||||
"name": "payment_customer_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "customer_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"payment_status_idx": {
|
||||
"name": "payment_status_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "status",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"payment_subscription_id_idx": {
|
||||
"name": "payment_subscription_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "subscription_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"payment_session_id_idx": {
|
||||
"name": "payment_session_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "session_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"payment_invoice_id_idx": {
|
||||
"name": "payment_invoice_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "invoice_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"payment_user_id_user_id_fk": {
|
||||
"name": "payment_user_id_user_id_fk",
|
||||
"tableFrom": "payment",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"payment_invoice_id_unique": {
|
||||
"name": "payment_invoice_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"invoice_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"impersonated_by": {
|
||||
"name": "impersonated_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_token_idx": {
|
||||
"name": "session_token_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "token",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"session_user_id_idx": {
|
||||
"name": "session_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"banned": {
|
||||
"name": "banned",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ban_reason": {
|
||||
"name": "ban_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ban_expires": {
|
||||
"name": "ban_expires",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_id_idx": {
|
||||
"name": "user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"user_customer_id_idx": {
|
||||
"name": "user_customer_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "customer_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"user_role_idx": {
|
||||
"name": "user_role_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "role",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_credit": {
|
||||
"name": "user_credit",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"current_credits": {
|
||||
"name": "current_credits",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"last_refresh_at": {
|
||||
"name": "last_refresh_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_credit_user_id_idx": {
|
||||
"name": "user_credit_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_credit_user_id_user_id_fk": {
|
||||
"name": "user_credit_user_id_user_id_fk",
|
||||
"tableFrom": "user_credit",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
@ -29,6 +29,13 @@
|
||||
"when": 1752992749001,
|
||||
"tag": "0003_loving_risque",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1757258758531,
|
||||
"tag": "0004_clever_molly_hayes",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -72,6 +72,7 @@ export const payment = pgTable("payment", {
|
||||
customerId: text('customer_id').notNull(),
|
||||
subscriptionId: text('subscription_id'),
|
||||
sessionId: text('session_id'),
|
||||
invoiceId: text('invoice_id').unique(), // unique constraint for avoiding duplicate processing
|
||||
status: text('status').notNull(),
|
||||
periodStart: timestamp('period_start'),
|
||||
periodEnd: timestamp('period_end'),
|
||||
@ -88,6 +89,7 @@ export const payment = pgTable("payment", {
|
||||
paymentStatusIdx: index("payment_status_idx").on(table.status),
|
||||
paymentSubscriptionIdIdx: index("payment_subscription_id_idx").on(table.subscriptionId),
|
||||
paymentSessionIdIdx: index("payment_session_id_idx").on(table.sessionId),
|
||||
paymentInvoiceIdIdx: index("payment_invoice_id_idx").on(table.invoiceId),
|
||||
}));
|
||||
|
||||
export const userCredit = pgTable("user_credit", {
|
||||
@ -108,7 +110,7 @@ export const creditTransaction = pgTable("credit_transaction", {
|
||||
description: text("description"),
|
||||
amount: integer("amount").notNull(),
|
||||
remainingAmount: integer("remaining_amount"),
|
||||
paymentId: text("payment_id"),
|
||||
paymentId: text("payment_id"), // payment id is actually invoice id
|
||||
expirationDate: timestamp("expiration_date"),
|
||||
expirationDateProcessedAt: timestamp("expiration_date_processed_at"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
|
@ -9,11 +9,7 @@ import { getCreditPackageById } from '@/credits/server';
|
||||
import { CREDIT_TRANSACTION_TYPE } from '@/credits/types';
|
||||
import { getDb } from '@/db';
|
||||
import { payment, user } from '@/db/schema';
|
||||
import {
|
||||
findPlanByPlanId,
|
||||
findPlanByPriceId,
|
||||
findPriceInPlan,
|
||||
} from '@/lib/price-plan';
|
||||
import { findPlanByPlanId, findPriceInPlan } from '@/lib/price-plan';
|
||||
import { sendNotification } from '@/notification/notification';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
import { Stripe } from 'stripe';
|
||||
@ -57,13 +53,7 @@ export class StripeProvider implements PaymentProvider {
|
||||
}
|
||||
|
||||
// Initialize Stripe without specifying apiVersion to use default/latest version
|
||||
// https://opennext.js.org/cloudflare/howtos/stripeAPI
|
||||
// When creating a Stripe object, the default http client implementation is based on
|
||||
// node:https which is not implemented on Workers.
|
||||
this.stripe = new Stripe(apiKey, {
|
||||
// Cloudflare Workers use the Fetch API for their API requests.
|
||||
httpClient: Stripe.createFetchHttpClient(),
|
||||
});
|
||||
this.stripe = new Stripe(apiKey);
|
||||
this.webhookSecret = webhookSecret;
|
||||
}
|
||||
|
||||
@ -498,6 +488,15 @@ export class StripeProvider implements PaymentProvider {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (eventType.startsWith('invoice.')) {
|
||||
// Handle invoice events
|
||||
switch (eventType) {
|
||||
case 'invoice.payment_succeeded': {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
await this.onInvoicePaymentSucceeded(invoice);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (eventType.startsWith('checkout.')) {
|
||||
// Handle checkout events
|
||||
if (eventType === 'checkout.session.completed') {
|
||||
@ -520,115 +519,426 @@ export class StripeProvider implements PaymentProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create payment record
|
||||
* Handle successful invoice payment - NEW ARCHITECTURE
|
||||
* Only create payment records here after payment is confirmed
|
||||
*
|
||||
* For one-time payments, the order of events may be:
|
||||
* checkout.session.completed
|
||||
* invoice.payment_succeeded
|
||||
*
|
||||
* For subscription payments, the order of events may be:
|
||||
* checkout.session.completed
|
||||
* customer.subscription.created
|
||||
* customer.subscription.updated
|
||||
* invoice.payment_succeeded
|
||||
*
|
||||
* @param invoice Stripe invoice
|
||||
*/
|
||||
private async onInvoicePaymentSucceeded(
|
||||
invoice: Stripe.Invoice
|
||||
): Promise<void> {
|
||||
console.log('>> Handle invoice payment succeeded');
|
||||
|
||||
try {
|
||||
const subscriptionId = invoice.subscription as string | null;
|
||||
|
||||
if (subscriptionId) {
|
||||
// This is a subscription payment
|
||||
await this.createSubscriptionPayment(invoice, subscriptionId);
|
||||
} else {
|
||||
// This is a one-time payment
|
||||
await this.createOneTimePayment(invoice);
|
||||
}
|
||||
|
||||
console.log('<< Successfully processed invoice payment');
|
||||
} catch (error) {
|
||||
console.error('<< Handle invoice payment succeeded error:', error);
|
||||
|
||||
// Check if it's a duplicate invoice error (database constraint violation)
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes('unique constraint')
|
||||
) {
|
||||
console.log('<< Invoice already processed:', invoice.id);
|
||||
return; // Don't throw, this is expected for duplicate processing
|
||||
}
|
||||
|
||||
// For other errors, let Stripe retry
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create subscription payment record and process benefits - NEW ARCHITECTURE
|
||||
*
|
||||
* The order of events may be:
|
||||
* checkout.session.completed
|
||||
* customer.subscription.created
|
||||
* customer.subscription.updated
|
||||
* invoice.payment_succeeded
|
||||
*
|
||||
* @param invoice Stripe invoice
|
||||
* @param subscriptionId Subscription ID
|
||||
*/
|
||||
private async createSubscriptionPayment(
|
||||
invoice: Stripe.Invoice,
|
||||
subscriptionId: string
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
'>> Create subscription payment record for subscription:',
|
||||
subscriptionId
|
||||
);
|
||||
|
||||
try {
|
||||
// Get subscription details from Stripe
|
||||
const subscription =
|
||||
await this.stripe.subscriptions.retrieve(subscriptionId);
|
||||
const customerId = subscription.customer as string;
|
||||
|
||||
// Get priceId from subscription items
|
||||
const priceId = subscription.items.data[0]?.price.id;
|
||||
if (!priceId) {
|
||||
console.warn('<< No priceId found for subscription');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get userId from subscription metadata or fallback to customerId lookup
|
||||
let userId: string | undefined = subscription.metadata.userId;
|
||||
|
||||
// If no userId in metadata (common in renewals), find by customerId
|
||||
if (!userId) {
|
||||
console.log('No userId in metadata, finding by customerId');
|
||||
userId = await this.findUserIdByCustomerId(customerId);
|
||||
|
||||
if (!userId) {
|
||||
console.error('<< No userId found, this should not happen');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const periodStart = this.getPeriodStart(subscription);
|
||||
const periodEnd = this.getPeriodEnd(subscription);
|
||||
const trialStart = subscription.trial_start
|
||||
? new Date(subscription.trial_start * 1000)
|
||||
: null;
|
||||
const trialEnd = subscription.trial_end
|
||||
? new Date(subscription.trial_end * 1000)
|
||||
: null;
|
||||
const currentDate = new Date();
|
||||
|
||||
// Create payment record with subscription status
|
||||
const db = await getDb();
|
||||
const paymentResult = await db
|
||||
.insert(payment)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
priceId,
|
||||
type: PaymentTypes.SUBSCRIPTION,
|
||||
userId,
|
||||
customerId,
|
||||
subscriptionId,
|
||||
invoiceId: invoice.id,
|
||||
interval: this.mapStripeIntervalToPlanInterval(subscription),
|
||||
status: this.mapSubscriptionStatusToPaymentStatus(
|
||||
subscription.status
|
||||
), // Use actual subscription status
|
||||
periodStart,
|
||||
periodEnd,
|
||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||
trialStart,
|
||||
trialEnd,
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
})
|
||||
.returning({ id: payment.id });
|
||||
|
||||
if (paymentResult.length === 0) {
|
||||
console.warn('<< Failed to create subscription payment record');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add subscription credits if enabled
|
||||
if (websiteConfig.credits?.enableCredits) {
|
||||
await addSubscriptionCredits(userId, priceId);
|
||||
console.log('Added subscription credits for invoice:', invoice.id);
|
||||
}
|
||||
|
||||
console.log('<< Successfully processed subscription payment');
|
||||
} catch (error) {
|
||||
console.error('<< Create subscription payment error:', error);
|
||||
|
||||
// Don't throw error if it's already processed
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes('unique constraint')
|
||||
) {
|
||||
console.log('<< Subscription payment already processed:', invoice.id);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one-time payment record and process benefits - NEW ARCHITECTURE
|
||||
*
|
||||
* The order of events may be:
|
||||
* checkout.session.completed
|
||||
* invoice.payment_succeeded
|
||||
*
|
||||
* @param invoice Stripe invoice
|
||||
*/
|
||||
private async createOneTimePayment(invoice: Stripe.Invoice): Promise<void> {
|
||||
console.log('>> Create one-time payment record for invoice:', invoice.id);
|
||||
|
||||
try {
|
||||
const customerId = invoice.customer as string;
|
||||
const paymentIntentId = invoice.payment_intent as string;
|
||||
|
||||
if (!paymentIntentId) {
|
||||
console.warn('<< No payment_intent found in invoice:', invoice.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get payment intent to access metadata
|
||||
const paymentIntent =
|
||||
await this.stripe.paymentIntents.retrieve(paymentIntentId);
|
||||
const metadata = paymentIntent.metadata;
|
||||
|
||||
// Get userId from payment intent metadata or fallback to customerId lookup
|
||||
let userId: string | undefined = metadata?.userId;
|
||||
if (!userId) {
|
||||
console.log('No userId in metadata, finding by customerId');
|
||||
userId = await this.findUserIdByCustomerId(customerId);
|
||||
|
||||
if (!userId) {
|
||||
console.error('<< No userId found, this should not happen');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a credit purchase
|
||||
const isCreditPurchase = metadata?.type === 'credit_purchase';
|
||||
|
||||
if (isCreditPurchase) {
|
||||
// Process credit purchase
|
||||
await this.createCreditPurchasePayment(invoice, metadata, userId);
|
||||
} else {
|
||||
// Process lifetime plan purchase
|
||||
await this.createLifetimePlanPayment(
|
||||
invoice,
|
||||
metadata,
|
||||
userId,
|
||||
customerId
|
||||
);
|
||||
}
|
||||
|
||||
console.log('<< Successfully created one-time payment record');
|
||||
} catch (error) {
|
||||
console.error('<< Create one-time payment error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create payment record for credit purchase - NEW ARCHITECTURE
|
||||
* @param invoice Stripe invoice
|
||||
* @param metadata Payment intent metadata
|
||||
* @param userId User ID
|
||||
*/
|
||||
private async createCreditPurchasePayment(
|
||||
invoice: Stripe.Invoice,
|
||||
metadata: { [key: string]: string },
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
console.log('>> Create credit purchase payment record');
|
||||
|
||||
try {
|
||||
const packageId = metadata.packageId;
|
||||
const credits = metadata.credits;
|
||||
const customerId = invoice.customer as string;
|
||||
|
||||
if (!packageId || !credits) {
|
||||
console.warn('<< Missing packageId or credits in metadata');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get credit package
|
||||
const creditPackage = getCreditPackageById(packageId);
|
||||
if (!creditPackage) {
|
||||
console.warn('<< Credit package not found:', packageId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create payment record
|
||||
const db = await getDb();
|
||||
const currentDate = new Date();
|
||||
const paymentResult = await db
|
||||
.insert(payment)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
priceId: metadata.priceId || '',
|
||||
type: PaymentTypes.ONE_TIME,
|
||||
userId,
|
||||
customerId,
|
||||
invoiceId: invoice.id,
|
||||
status: 'completed',
|
||||
periodStart: currentDate,
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
})
|
||||
.returning({ id: payment.id });
|
||||
|
||||
if (paymentResult.length === 0) {
|
||||
console.warn('<< Failed to create credit purchase payment record');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add credits to user account
|
||||
const amount = invoice.amount_paid ? invoice.amount_paid / 100 : 0;
|
||||
await addCredits({
|
||||
userId,
|
||||
amount: Number.parseInt(credits),
|
||||
type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE,
|
||||
description: `+${credits} credits for package ${packageId} ($${amount.toLocaleString()})`,
|
||||
paymentId: invoice.id, // Use invoice ID as payment ID
|
||||
expireDays: creditPackage.expireDays,
|
||||
});
|
||||
|
||||
console.log('<< Successfully added credits to user for credit purchase');
|
||||
} catch (error) {
|
||||
console.error('<< Create credit purchase payment error:', error);
|
||||
|
||||
// Don't throw error if it's already processed
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes('unique constraint')
|
||||
) {
|
||||
console.log('<< Credit purchase already processed:', invoice.id);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create payment record for lifetime plan purchase - NEW ARCHITECTURE
|
||||
* @param invoice Stripe invoice
|
||||
* @param metadata Payment intent metadata
|
||||
* @param userId User ID
|
||||
* @param customerId Customer ID
|
||||
*/
|
||||
private async createLifetimePlanPayment(
|
||||
invoice: Stripe.Invoice,
|
||||
metadata: { [key: string]: string },
|
||||
userId: string,
|
||||
customerId: string
|
||||
): Promise<void> {
|
||||
console.log('>> Create lifetime plan payment record');
|
||||
|
||||
try {
|
||||
const priceId = metadata?.priceId;
|
||||
if (!priceId) {
|
||||
console.warn('<< No priceId found in payment intent metadata');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create payment record
|
||||
const db = await getDb();
|
||||
const currentDate = new Date();
|
||||
const paymentResult = await db
|
||||
.insert(payment)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
priceId,
|
||||
type: PaymentTypes.ONE_TIME,
|
||||
userId,
|
||||
customerId,
|
||||
invoiceId: invoice.id,
|
||||
status: 'completed',
|
||||
periodStart: currentDate,
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
})
|
||||
.returning({ id: payment.id });
|
||||
|
||||
if (paymentResult.length === 0) {
|
||||
console.warn('<< Failed to create lifetime plan payment record');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add lifetime credits if enabled
|
||||
if (websiteConfig.credits?.enableCredits) {
|
||||
await addLifetimeMonthlyCredits(userId, priceId);
|
||||
console.log('Added lifetime credits for invoice:', invoice.id);
|
||||
}
|
||||
|
||||
// Send notification
|
||||
const amount = invoice.amount_paid ? invoice.amount_paid / 100 : 0;
|
||||
await sendNotification(invoice.id, customerId, userId, amount);
|
||||
|
||||
console.log('<< Successfully created lifetime plan payment record');
|
||||
} catch (error) {
|
||||
console.error('<< Create lifetime plan payment error:', error);
|
||||
|
||||
// Don't throw error if it's already processed
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes('unique constraint')
|
||||
) {
|
||||
console.log('<< Lifetime plan payment already processed:', invoice.id);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle subscription creation - NEW ARCHITECTURE
|
||||
* Only log the event, payment records created in invoice.payment_succeeded
|
||||
* @param stripeSubscription Stripe subscription
|
||||
*/
|
||||
private async onCreateSubscription(
|
||||
stripeSubscription: Stripe.Subscription
|
||||
): Promise<void> {
|
||||
console.log('>> Create payment record for Stripe subscription');
|
||||
const customerId = stripeSubscription.customer as string;
|
||||
|
||||
// get priceId from subscription items (this is always available)
|
||||
const priceId = stripeSubscription.items.data[0]?.price.id;
|
||||
if (!priceId) {
|
||||
console.warn('No priceId found for subscription');
|
||||
return;
|
||||
}
|
||||
|
||||
// get userId from metadata, we add it in the createCheckout session
|
||||
const userId = stripeSubscription.metadata.userId;
|
||||
if (!userId) {
|
||||
console.warn('No userId found for subscription');
|
||||
return;
|
||||
}
|
||||
|
||||
const periodStart = this.getPeriodStart(stripeSubscription);
|
||||
const periodEnd = this.getPeriodEnd(stripeSubscription);
|
||||
|
||||
// create fields
|
||||
const createFields: any = {
|
||||
id: randomUUID(),
|
||||
priceId: priceId,
|
||||
type: PaymentTypes.SUBSCRIPTION,
|
||||
userId: userId,
|
||||
customerId: customerId,
|
||||
subscriptionId: stripeSubscription.id,
|
||||
interval: this.mapStripeIntervalToPlanInterval(stripeSubscription),
|
||||
status: this.mapSubscriptionStatusToPaymentStatus(
|
||||
stripeSubscription.status
|
||||
),
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
||||
trialStart: stripeSubscription.trial_start
|
||||
? new Date(stripeSubscription.trial_start * 1000)
|
||||
: null,
|
||||
trialEnd: stripeSubscription.trial_end
|
||||
? new Date(stripeSubscription.trial_end * 1000)
|
||||
: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const db = await getDb();
|
||||
const result = await db
|
||||
.insert(payment)
|
||||
.values(createFields)
|
||||
.returning({ id: payment.id });
|
||||
|
||||
if (result.length > 0) {
|
||||
console.log('<< Created new payment record for Stripe subscription');
|
||||
} else {
|
||||
console.warn('<< No payment record created for Stripe subscription');
|
||||
}
|
||||
|
||||
// Conditionally handle credits after subscription creation if enables credits
|
||||
if (websiteConfig.credits?.enableCredits) {
|
||||
await addSubscriptionCredits(userId, priceId);
|
||||
console.log('<< Added subscription monthly credits for user');
|
||||
}
|
||||
console.log('Handle subscription creation:', stripeSubscription.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update payment record
|
||||
*
|
||||
* When subscription is renewed, the order of events may be:
|
||||
* customer.subscription.updated
|
||||
* invoice.payment_succeeded
|
||||
*
|
||||
* In this case, we need to update the payment record.
|
||||
*
|
||||
* @param stripeSubscription Stripe subscription
|
||||
*/
|
||||
private async onUpdateSubscription(
|
||||
stripeSubscription: Stripe.Subscription
|
||||
): Promise<void> {
|
||||
console.log('>> Update payment record for Stripe subscription');
|
||||
console.log('>> Handle subscription update');
|
||||
|
||||
// get priceId from subscription items (this is always available)
|
||||
const priceId = stripeSubscription.items.data[0]?.price.id;
|
||||
if (!priceId) {
|
||||
console.warn('No priceId found for subscription');
|
||||
console.warn('<< No priceId found for subscription');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current payment record to check for period changes (indicating renewal)
|
||||
const db = await getDb();
|
||||
const payments = await db
|
||||
.select({
|
||||
userId: payment.userId,
|
||||
periodStart: payment.periodStart,
|
||||
periodEnd: payment.periodEnd,
|
||||
})
|
||||
.from(payment)
|
||||
.where(eq(payment.subscriptionId, stripeSubscription.id))
|
||||
.limit(1);
|
||||
|
||||
// get new period start and end
|
||||
const newPeriodStart = this.getPeriodStart(stripeSubscription);
|
||||
const newPeriodEnd = this.getPeriodEnd(stripeSubscription);
|
||||
|
||||
// Check if this is a renewal (period has changed and subscription is active)
|
||||
const isRenewal =
|
||||
payments.length > 0 &&
|
||||
stripeSubscription.status === 'active' &&
|
||||
payments[0].periodStart &&
|
||||
newPeriodStart &&
|
||||
payments[0].periodStart.getTime() !== newPeriodStart.getTime();
|
||||
const trialStart = stripeSubscription.trial_start
|
||||
? new Date(stripeSubscription.trial_start * 1000)
|
||||
: undefined;
|
||||
const trialEnd = stripeSubscription.trial_end
|
||||
? new Date(stripeSubscription.trial_end * 1000)
|
||||
: undefined;
|
||||
|
||||
// update fields
|
||||
const updateFields: any = {
|
||||
@ -640,15 +950,12 @@ export class StripeProvider implements PaymentProvider {
|
||||
periodStart: newPeriodStart,
|
||||
periodEnd: newPeriodEnd,
|
||||
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
||||
trialStart: stripeSubscription.trial_start
|
||||
? new Date(stripeSubscription.trial_start * 1000)
|
||||
: undefined,
|
||||
trialEnd: stripeSubscription.trial_end
|
||||
? new Date(stripeSubscription.trial_end * 1000)
|
||||
: undefined,
|
||||
trialStart: trialStart,
|
||||
trialEnd: trialEnd,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const db = await getDb();
|
||||
const result = await db
|
||||
.update(payment)
|
||||
.set(updateFields)
|
||||
@ -656,24 +963,9 @@ export class StripeProvider implements PaymentProvider {
|
||||
.returning({ id: payment.id });
|
||||
|
||||
if (result.length > 0) {
|
||||
console.log('<< Updated payment record for Stripe subscription');
|
||||
|
||||
// Add credits for subscription renewal
|
||||
const currentPayment = payments[0];
|
||||
const userId = currentPayment.userId;
|
||||
// Add subscription renewal credits if plan config enables credits
|
||||
if (isRenewal && userId && websiteConfig.credits?.enableCredits) {
|
||||
// Note: For yearly subscriptions, this webhook only triggers once per year
|
||||
// Monthly credits for yearly subscribers are handled by the distributeCreditsToAllUsers cron job
|
||||
await addSubscriptionCredits(userId, priceId);
|
||||
console.log('<< Added subscription renewal credits for user');
|
||||
} else {
|
||||
console.log(
|
||||
'<< No renewal credits added for user, isRenewal: ' + isRenewal
|
||||
);
|
||||
}
|
||||
console.log('<< Updated payment record for subscription');
|
||||
} else {
|
||||
console.warn('<< No payment record found for Stripe subscription');
|
||||
console.warn('<< No payment record found for subscription update');
|
||||
}
|
||||
}
|
||||
|
||||
@ -684,7 +976,8 @@ export class StripeProvider implements PaymentProvider {
|
||||
private async onDeleteSubscription(
|
||||
stripeSubscription: Stripe.Subscription
|
||||
): Promise<void> {
|
||||
console.log('>> Mark payment record for Stripe subscription as canceled');
|
||||
console.log('>> Handle subscription deletion');
|
||||
|
||||
const db = await getDb();
|
||||
const result = await db
|
||||
.update(payment)
|
||||
@ -700,177 +993,30 @@ export class StripeProvider implements PaymentProvider {
|
||||
if (result.length > 0) {
|
||||
console.log('<< Marked payment record for subscription as canceled');
|
||||
} else {
|
||||
console.warn(
|
||||
'<< No payment record found to cancel for Stripe subscription'
|
||||
);
|
||||
console.warn('<< No payment record found for subscription deletion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle one-time payment
|
||||
* Handle checkout session completion - NEW ARCHITECTURE
|
||||
* Only log the event, payment records created in invoice.payment_succeeded
|
||||
* @param session Stripe checkout session
|
||||
*/
|
||||
private async onOnetimePayment(
|
||||
session: Stripe.Checkout.Session
|
||||
): Promise<void> {
|
||||
const customerId = session.customer as string;
|
||||
console.log('>> Handle onetime payment for customer');
|
||||
|
||||
// get userId from session metadata, we add it in the createCheckout session
|
||||
const userId = session.metadata?.userId;
|
||||
if (!userId) {
|
||||
console.warn('No userId found for checkout session');
|
||||
return;
|
||||
}
|
||||
|
||||
// get priceId from session metadata, not from line items
|
||||
// const priceId = session.line_items?.data[0]?.price?.id;
|
||||
const priceId = session.metadata?.priceId;
|
||||
if (!priceId) {
|
||||
console.warn('No priceId found for checkout session');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// Check if this session has already been processed to prevent duplicate processing
|
||||
const existingPayment = await db
|
||||
.select({ id: payment.id })
|
||||
.from(payment)
|
||||
.where(eq(payment.sessionId, session.id))
|
||||
.limit(1);
|
||||
|
||||
if (existingPayment.length > 0) {
|
||||
console.log(
|
||||
'One-time payment session already processed: ' + session.id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a one-time payment record
|
||||
const now = new Date();
|
||||
const result = await db
|
||||
.insert(payment)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
priceId: priceId,
|
||||
type: PaymentTypes.ONE_TIME,
|
||||
userId: userId,
|
||||
customerId: customerId,
|
||||
sessionId: session.id, // Track the session ID
|
||||
status: 'completed', // One-time payments are always completed
|
||||
periodStart: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.returning({ id: payment.id });
|
||||
|
||||
if (result.length === 0) {
|
||||
console.warn('<< Failed to create one-time payment record for user');
|
||||
return;
|
||||
}
|
||||
console.log('Created one-time payment record for user');
|
||||
|
||||
// Conditionally handle credits after one-time payment
|
||||
if (websiteConfig.credits?.enableCredits) {
|
||||
// For now, one time payment is only for lifetime plan
|
||||
await addLifetimeMonthlyCredits(userId, priceId);
|
||||
console.log('<< Added lifetime monthly credits for user');
|
||||
}
|
||||
|
||||
// Send notification
|
||||
const amount = session.amount_total ? session.amount_total / 100 : 0;
|
||||
await sendNotification(session.id, customerId, userId, amount);
|
||||
} catch (error) {
|
||||
console.error('onOnetimePayment error for session: ' + session.id, error);
|
||||
throw error;
|
||||
}
|
||||
console.log('Handle checkout session completion:', session.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle credit purchase
|
||||
* Handle credit purchase checkout completion - NEW ARCHITECTURE
|
||||
* Only log the event, payment records created in invoice.payment_succeeded
|
||||
* @param session Stripe checkout session
|
||||
*/
|
||||
private async onCreditPurchase(
|
||||
session: Stripe.Checkout.Session
|
||||
): Promise<void> {
|
||||
const customerId = session.customer as string;
|
||||
console.log('>> Handle credit purchase for customer');
|
||||
|
||||
// get userId from session metadata, we add it in the createCheckout session
|
||||
const userId = session.metadata?.userId;
|
||||
if (!userId) {
|
||||
console.warn('No userId found for checkout session');
|
||||
return;
|
||||
}
|
||||
|
||||
// get packageId from session metadata
|
||||
const packageId = session.metadata?.packageId;
|
||||
if (!packageId) {
|
||||
console.warn('No packageId found for checkout session');
|
||||
return;
|
||||
}
|
||||
|
||||
// get credits from session metadata
|
||||
const credits = session.metadata?.credits;
|
||||
if (!credits) {
|
||||
console.warn('No credits found for checkout session');
|
||||
return;
|
||||
}
|
||||
|
||||
// get credit package
|
||||
const creditPackage = getCreditPackageById(packageId);
|
||||
if (!creditPackage) {
|
||||
console.warn('Credit package ' + packageId + ' not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if this session has already been processed to prevent duplicate credit additions
|
||||
const db = await getDb();
|
||||
const existingPayment = await db
|
||||
.select({ id: payment.id })
|
||||
.from(payment)
|
||||
.where(eq(payment.sessionId, session.id))
|
||||
.limit(1);
|
||||
|
||||
if (existingPayment.length > 0) {
|
||||
console.log('Credit purchase session already processed: ' + session.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create payment record first to mark this session as processed
|
||||
const now = new Date();
|
||||
await db.insert(payment).values({
|
||||
id: randomUUID(),
|
||||
priceId: session.metadata?.priceId || '',
|
||||
type: PaymentTypes.ONE_TIME,
|
||||
userId: userId,
|
||||
customerId: customerId,
|
||||
sessionId: session.id, // Use sessionId to track processed sessions
|
||||
status: 'completed',
|
||||
periodStart: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
// add credits to user account
|
||||
const amount = session.amount_total ? session.amount_total / 100 : 0;
|
||||
await addCredits({
|
||||
userId,
|
||||
amount: Number.parseInt(credits),
|
||||
type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE,
|
||||
description: `+${credits} credits for package ${packageId} ($${amount.toLocaleString()})`,
|
||||
paymentId: session.id,
|
||||
expireDays: creditPackage.expireDays,
|
||||
});
|
||||
|
||||
console.log('Added ' + credits + ' credits to user');
|
||||
} catch (error) {
|
||||
console.error('onCreditPurchase error for session: ' + session.id, error);
|
||||
throw error;
|
||||
}
|
||||
console.log('Handle credit purchase checkout completion:', session.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,84 +0,0 @@
|
||||
/**
|
||||
* For more details on how to configure Wrangler, refer to:
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
*/
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"main": ".open-next/worker.js",
|
||||
"name": "mksaas-template",
|
||||
"compatibility_date": "2024-12-30",
|
||||
"compatibility_flags": [
|
||||
// Enable Node.js API
|
||||
// see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag
|
||||
"nodejs_compat",
|
||||
// This also enables nodejs_compat_v2 as long as compatibility date is 2024-09-23 or later.
|
||||
// https://developers.cloudflare.com/workers/configuration/compatibility-dates/#nodejs-compatibility-flag
|
||||
// Enable improved Node.js API with polyfills and native code
|
||||
// see https://blog.cloudflare.com/zh-cn/more-npm-packages-on-cloudflare-workers-combining-polyfills-and-native-code/
|
||||
// "nodejs_compat_v2",
|
||||
// Enable auto-populating process.env
|
||||
// see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-auto-populating-processenv
|
||||
"nodejs_compat_populate_process_env",
|
||||
// Allow to fetch URLs in your app
|
||||
// see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
// Minification helps to keep the Worker bundle size down and improve start up time.
|
||||
"minify": true,
|
||||
|
||||
// Enables Workers Trace Events Logpush for a Worker
|
||||
"logpush": true,
|
||||
|
||||
// https://developers.cloudflare.com/workers/wrangler/configuration/#top-level-only-keys
|
||||
// Whether Wrangler should keep variables configured in the dashboard on deploy
|
||||
"keep_vars": true,
|
||||
|
||||
"assets": {
|
||||
"binding": "ASSETS",
|
||||
"directory": ".open-next/assets"
|
||||
},
|
||||
|
||||
// https://developers.cloudflare.com/workers/wrangler/configuration/#observability
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
/**
|
||||
* Smart Placement
|
||||
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||
*/
|
||||
// "placement": { "mode": "smart" },
|
||||
|
||||
/**
|
||||
* Bindings
|
||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||
* databases, object storage, AI inference, real-time communication and more.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hyperdrive
|
||||
* https://opennext.js.org/cloudflare/howtos/db#hyperdrive-example
|
||||
* https://developers.cloudflare.com/workers/tutorials/postgres/#8-use-hyperdrive-to-accelerate-queries
|
||||
*/
|
||||
"hyperdrive": [
|
||||
{
|
||||
"binding": "HYPERDRIVE",
|
||||
"id": "8ba4508b28cf42f987f3533c1f09433c",
|
||||
"localConnectionString": "postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
}
|
||||
],
|
||||
|
||||
/**
|
||||
* Environment Variables
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||
*/
|
||||
"vars": {},
|
||||
|
||||
/**
|
||||
* Note: Use secrets to store sensitive data.
|
||||
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||
*/
|
||||
|
||||
"kv_namespaces": []
|
||||
}
|
Loading…
Reference in New Issue
Block a user