Merge remote-tracking branch 'origin/main' into cloudflare
This commit is contained in:
commit
97654d97ea
13
env.example
13
env.example
@ -168,10 +168,10 @@ TURNSTILE_SECRET_KEY=""
|
||||
NEXT_PUBLIC_CRISP_WEBSITE_ID=""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Inngest
|
||||
# https://mksaas.com/docs/jobs#setup
|
||||
# Cron Jobs
|
||||
# -----------------------------------------------------------------------------
|
||||
INNGEST_SIGNING_KEY=""
|
||||
CRON_JOBS_USERNAME=""
|
||||
CRON_JOBS_PASSWORD=""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# AI
|
||||
@ -185,6 +185,13 @@ GOOGLE_GENERATIVE_AI_API_KEY=""
|
||||
DEEPSEEK_API_KEY=""
|
||||
OPENROUTER_API_KEY=""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Basic Authentication
|
||||
# Used for protecting sensitive API endpoints like distribute-credits
|
||||
# -----------------------------------------------------------------------------
|
||||
BASIC_AUTH_USERNAME="admin"
|
||||
BASIC_AUTH_PASSWORD=""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Web Content Analyzer (Firecrawl)
|
||||
# https://firecrawl.dev/
|
||||
|
@ -101,7 +101,6 @@
|
||||
"fumadocs-core": "^15.6.7",
|
||||
"fumadocs-mdx": "^11.7.3",
|
||||
"fumadocs-ui": "^15.6.7",
|
||||
"inngest": "^3.40.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"motion": "^12.4.3",
|
||||
|
1637
pnpm-lock.yaml
generated
1637
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
58
src/app/api/distribute-credits/route.ts
Normal file
58
src/app/api/distribute-credits/route.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { distributeCreditsToAllUsers } from '@/credits/credits';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
// Basic authentication middleware
|
||||
function validateBasicAuth(request: Request): boolean {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract credentials from Authorization header
|
||||
const base64Credentials = authHeader.split(' ')[1];
|
||||
const credentials = Buffer.from(base64Credentials, 'base64').toString(
|
||||
'utf-8'
|
||||
);
|
||||
const [username, password] = credentials.split(':');
|
||||
|
||||
// Validate against environment variables
|
||||
const expectedUsername = process.env.CRON_JOBS_USERNAME;
|
||||
const expectedPassword = process.env.CRON_JOBS_PASSWORD;
|
||||
|
||||
if (!expectedUsername || !expectedPassword) {
|
||||
console.error(
|
||||
'Basic auth credentials not configured in environment variables'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return username === expectedUsername && password === expectedPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* distribute credits to all users daily
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
// Validate basic authentication
|
||||
if (!validateBasicAuth(request)) {
|
||||
console.error('distribute credits unauthorized');
|
||||
return new NextResponse('Unauthorized', {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': 'Basic realm="Secure Area"',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('distribute credits start');
|
||||
const { processedCount, errorCount } = await distributeCreditsToAllUsers();
|
||||
console.log(
|
||||
`distribute credits end, processed: ${processedCount}, errors: ${errorCount}`
|
||||
);
|
||||
return NextResponse.json({
|
||||
message: `distribute credits success, processed: ${processedCount}, errors: ${errorCount}`,
|
||||
processedCount,
|
||||
errorCount,
|
||||
});
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { inngest } from '@/inngest/client';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
// Opt out of caching; every request should send a new event
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Create a simple async Next.js API route handler
|
||||
export async function GET() {
|
||||
console.log('Send event to Inngest start');
|
||||
// Send your event payload to Inngest
|
||||
await inngest.send({
|
||||
name: 'test/hello.world',
|
||||
data: {
|
||||
email: 'testUser@example.com',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Send event to Inngest end');
|
||||
return NextResponse.json({ message: 'Event sent!' });
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { serve } from 'inngest/next';
|
||||
import { inngest } from '../../../inngest/client';
|
||||
import { distributeCreditsDaily, helloWorld } from '../../../inngest/functions';
|
||||
|
||||
/**
|
||||
* Inngest route
|
||||
*
|
||||
* https://www.inngest.com/docs/getting-started/nextjs-quick-start
|
||||
*
|
||||
* Next.js Edge Functions hosted on Vercel can also stream responses back to Inngest,
|
||||
* giving you a much higher request timeout of 15 minutes (up from 10 seconds on the Vercel Hobby plan!).
|
||||
* To enable this, set your runtime to "edge" (see Quickstart for Using Edge Functions | Vercel Docs)
|
||||
* and add the streaming: "allow" option to your serve handler:
|
||||
* https://www.inngest.com/docs/learn/serving-inngest-functions#framework-next-js
|
||||
*/
|
||||
export const { GET, POST, PUT } = serve({
|
||||
client: inngest,
|
||||
functions: [helloWorld, distributeCreditsDaily],
|
||||
});
|
@ -4,7 +4,8 @@ import { getDb } from '@/db';
|
||||
import { creditTransaction, payment, user, userCredit } from '@/db/schema';
|
||||
import { findPlanByPriceId } from '@/lib/price-plan';
|
||||
import { addDays, isAfter } from 'date-fns';
|
||||
import { and, asc, desc, eq, gt, isNull, not, or } from 'drizzle-orm';
|
||||
import { and, asc, desc, eq, gt, inArray, isNull, not, or } from 'drizzle-orm';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { CREDIT_TRANSACTION_TYPE } from './types';
|
||||
|
||||
/**
|
||||
@ -447,6 +448,9 @@ export async function addMonthlyFreeCredits(userId: string) {
|
||||
expireDays,
|
||||
});
|
||||
|
||||
// Update last refresh time for free monthly credits
|
||||
await updateUserLastRefreshAt(userId, now);
|
||||
|
||||
console.log(
|
||||
`addMonthlyFreeCredits, ${credits} credits for user ${userId}, date: ${now.getFullYear()}-${now.getMonth() + 1}`
|
||||
);
|
||||
@ -559,65 +563,367 @@ export async function addLifetimeMonthlyCredits(userId: string) {
|
||||
* This function is designed to be called by a cron job
|
||||
*/
|
||||
export async function distributeCreditsToAllUsers() {
|
||||
console.log('distributing credits to all users start');
|
||||
console.log('distribute credits start');
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// Get all users with their current active payments/subscriptions
|
||||
const users = await db
|
||||
// Get all users with their current active payments/subscriptions in a single query
|
||||
// This uses a LEFT JOIN to get users and their latest active payment in one query
|
||||
const latestPaymentQuery = db
|
||||
.select({
|
||||
userId: payment.userId,
|
||||
priceId: payment.priceId,
|
||||
status: payment.status,
|
||||
createdAt: payment.createdAt,
|
||||
rowNumber:
|
||||
sql<number>`ROW_NUMBER() OVER (PARTITION BY ${payment.userId} ORDER BY ${payment.createdAt} DESC)`.as(
|
||||
'row_number'
|
||||
),
|
||||
})
|
||||
.from(payment)
|
||||
.where(or(eq(payment.status, 'active'), eq(payment.status, 'trialing')))
|
||||
.as('latest_payment');
|
||||
|
||||
const usersWithPayments = await db
|
||||
.select({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
priceId: latestPaymentQuery.priceId,
|
||||
paymentStatus: latestPaymentQuery.status,
|
||||
paymentCreatedAt: latestPaymentQuery.createdAt,
|
||||
})
|
||||
.from(user)
|
||||
.where(eq(user.banned, false)); // Only active users
|
||||
console.log('distributing credits to all users, users count:', users.length);
|
||||
.leftJoin(
|
||||
latestPaymentQuery,
|
||||
and(
|
||||
eq(user.id, latestPaymentQuery.userId),
|
||||
eq(latestPaymentQuery.rowNumber, 1)
|
||||
)
|
||||
)
|
||||
.where(or(isNull(user.banned), eq(user.banned, false)));
|
||||
|
||||
console.log('distribute credits, users count:', usersWithPayments.length);
|
||||
|
||||
let processedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const userRecord of users) {
|
||||
try {
|
||||
// Get user's current active subscription/payment
|
||||
const activePayments = await db
|
||||
.select()
|
||||
.from(payment)
|
||||
.where(
|
||||
and(
|
||||
eq(payment.userId, userRecord.userId),
|
||||
or(eq(payment.status, 'active'), eq(payment.status, 'trialing'))
|
||||
)
|
||||
)
|
||||
.orderBy(desc(payment.createdAt));
|
||||
// Separate users by their plan type for batch processing
|
||||
const lifetimeUserIds: string[] = [];
|
||||
const freeUserIds: string[] = [];
|
||||
|
||||
if (activePayments.length > 0) {
|
||||
// User has active subscription - check what type
|
||||
const activePayment = activePayments[0];
|
||||
const pricePlan = findPlanByPriceId(activePayment.priceId);
|
||||
|
||||
if (pricePlan?.isLifetime) {
|
||||
// Lifetime user - add monthly credits
|
||||
await addLifetimeMonthlyCredits(userRecord.userId);
|
||||
}
|
||||
// Note: Subscription renewals are handled by Stripe webhooks, not here
|
||||
} else {
|
||||
// User has no active subscription - add free monthly credits if enabled
|
||||
await addMonthlyFreeCredits(userRecord.userId);
|
||||
usersWithPayments.forEach((userRecord) => {
|
||||
if (userRecord.priceId && userRecord.paymentStatus) {
|
||||
// User has active subscription - check what type
|
||||
const pricePlan = findPlanByPriceId(userRecord.priceId);
|
||||
if (pricePlan?.isLifetime) {
|
||||
lifetimeUserIds.push(userRecord.userId);
|
||||
}
|
||||
// Note: Subscription renewals are handled by Stripe webhooks, not here
|
||||
} else {
|
||||
// User has no active subscription - add free monthly credits if enabled
|
||||
freeUserIds.push(userRecord.userId);
|
||||
}
|
||||
});
|
||||
|
||||
processedCount++;
|
||||
console.log(
|
||||
`distribute credits, lifetime users: ${lifetimeUserIds.length}, free users: ${freeUserIds.length}`
|
||||
);
|
||||
|
||||
// Process lifetime users in batches
|
||||
const batchSize = 100;
|
||||
for (let i = 0; i < lifetimeUserIds.length; i += batchSize) {
|
||||
const batch = lifetimeUserIds.slice(i, i + batchSize);
|
||||
try {
|
||||
await batchAddLifetimeMonthlyCredits(batch);
|
||||
processedCount += batch.length;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`distributing credits to all users error, user: ${userRecord.userId}, error:`,
|
||||
`batchAddLifetimeMonthlyCredits error for batch ${i / batchSize + 1}:`,
|
||||
error
|
||||
);
|
||||
errorCount++;
|
||||
errorCount += batch.length;
|
||||
}
|
||||
|
||||
// Log progress for large datasets
|
||||
if (lifetimeUserIds.length > 1000) {
|
||||
console.log(
|
||||
`lifetime credits progress: ${Math.min(i + batchSize, lifetimeUserIds.length)}/${lifetimeUserIds.length}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process free users in batches
|
||||
for (let i = 0; i < freeUserIds.length; i += batchSize) {
|
||||
const batch = freeUserIds.slice(i, i + batchSize);
|
||||
try {
|
||||
await batchAddMonthlyFreeCredits(batch);
|
||||
processedCount += batch.length;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`batchAddMonthlyFreeCredits error for batch ${i / batchSize + 1}:`,
|
||||
error
|
||||
);
|
||||
errorCount += batch.length;
|
||||
}
|
||||
|
||||
// Log progress for large datasets
|
||||
if (freeUserIds.length > 1000) {
|
||||
console.log(
|
||||
`free credits progress: ${Math.min(i + batchSize, freeUserIds.length)}/${freeUserIds.length}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`distributing credits to all users end, processed: ${processedCount}, errors: ${errorCount}`
|
||||
`distribute credits end, processed: ${processedCount}, errors: ${errorCount}`
|
||||
);
|
||||
return { processedCount, errorCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch add monthly free credits to multiple users
|
||||
* @param userIds - Array of user IDs
|
||||
*/
|
||||
export async function batchAddMonthlyFreeCredits(userIds: string[]) {
|
||||
if (userIds.length === 0) return;
|
||||
|
||||
const freePlan = Object.values(websiteConfig.price.plans).find(
|
||||
(plan) => plan.isFree && !plan.disabled
|
||||
);
|
||||
if (!freePlan) {
|
||||
console.log('batchAddMonthlyFreeCredits, no free plan found');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
freePlan.disabled ||
|
||||
!freePlan.credits?.enable ||
|
||||
!freePlan.credits?.amount
|
||||
) {
|
||||
console.log(
|
||||
'batchAddMonthlyFreeCredits, plan disabled or credits disabled',
|
||||
freePlan.id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const now = new Date();
|
||||
const credits = freePlan.credits.amount;
|
||||
const expireDays = freePlan.credits.expireDays;
|
||||
|
||||
// Use transaction for data consistency
|
||||
let processedCount = 0;
|
||||
await db.transaction(async (tx) => {
|
||||
// Get all user credit records in one query
|
||||
const userCredits = await tx
|
||||
.select({
|
||||
userId: userCredit.userId,
|
||||
lastRefreshAt: userCredit.lastRefreshAt,
|
||||
currentCredits: userCredit.currentCredits,
|
||||
})
|
||||
.from(userCredit)
|
||||
.where(inArray(userCredit.userId, userIds));
|
||||
|
||||
// Create a map for quick lookup
|
||||
const userCreditMap = new Map(
|
||||
userCredits.map((record) => [record.userId, record])
|
||||
);
|
||||
|
||||
// Filter users who can receive credits
|
||||
const eligibleUserIds = userIds.filter((userId) => {
|
||||
const record = userCreditMap.get(userId);
|
||||
if (!record?.lastRefreshAt) {
|
||||
return true; // never added credits before
|
||||
}
|
||||
const last = new Date(record.lastRefreshAt);
|
||||
// different month or year means new month
|
||||
return (
|
||||
now.getMonth() !== last.getMonth() ||
|
||||
now.getFullYear() !== last.getFullYear()
|
||||
);
|
||||
});
|
||||
|
||||
if (eligibleUserIds.length === 0) {
|
||||
console.log('batchAddMonthlyFreeCredits, no eligible users');
|
||||
return;
|
||||
}
|
||||
|
||||
processedCount = eligibleUserIds.length;
|
||||
const expirationDate = expireDays ? addDays(now, expireDays) : undefined;
|
||||
|
||||
// Batch insert credit transactions
|
||||
const transactions = eligibleUserIds.map((userId) => ({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
type: CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH,
|
||||
amount: credits,
|
||||
remainingAmount: credits,
|
||||
description: `Free monthly credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`,
|
||||
expirationDate,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
|
||||
await tx.insert(creditTransaction).values(transactions);
|
||||
|
||||
// Prepare user credit updates
|
||||
const existingUserIds = eligibleUserIds.filter((userId) =>
|
||||
userCreditMap.has(userId)
|
||||
);
|
||||
const newUserIds = eligibleUserIds.filter(
|
||||
(userId) => !userCreditMap.has(userId)
|
||||
);
|
||||
|
||||
// Insert new user credit records
|
||||
if (newUserIds.length > 0) {
|
||||
const newRecords = newUserIds.map((userId) => ({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
currentCredits: credits,
|
||||
lastRefreshAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
await tx.insert(userCredit).values(newRecords);
|
||||
}
|
||||
|
||||
// Update existing user credit records
|
||||
if (existingUserIds.length > 0) {
|
||||
await tx
|
||||
.update(userCredit)
|
||||
.set({
|
||||
currentCredits: credits,
|
||||
lastRefreshAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(inArray(userCredit.userId, existingUserIds));
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`batchAddMonthlyFreeCredits, ${credits} credits for ${processedCount} users, date: ${now.getFullYear()}-${now.getMonth() + 1}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch add lifetime monthly credits to multiple users
|
||||
* @param userIds - Array of user IDs
|
||||
*/
|
||||
export async function batchAddLifetimeMonthlyCredits(userIds: string[]) {
|
||||
if (userIds.length === 0) return;
|
||||
|
||||
const lifetimePlan = Object.values(websiteConfig.price.plans).find(
|
||||
(plan) => plan.isLifetime && !plan.disabled
|
||||
);
|
||||
if (
|
||||
!lifetimePlan ||
|
||||
lifetimePlan.disabled ||
|
||||
!lifetimePlan.credits ||
|
||||
!lifetimePlan.credits.enable
|
||||
) {
|
||||
console.log(
|
||||
'batchAddLifetimeMonthlyCredits, plan disabled or credits disabled',
|
||||
lifetimePlan?.id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const now = new Date();
|
||||
const credits = lifetimePlan.credits.amount;
|
||||
const expireDays = lifetimePlan.credits.expireDays;
|
||||
|
||||
// Use transaction for data consistency
|
||||
let processedCount = 0;
|
||||
await db.transaction(async (tx) => {
|
||||
// Get all user credit records in one query
|
||||
const userCredits = await tx
|
||||
.select({
|
||||
userId: userCredit.userId,
|
||||
lastRefreshAt: userCredit.lastRefreshAt,
|
||||
currentCredits: userCredit.currentCredits,
|
||||
})
|
||||
.from(userCredit)
|
||||
.where(inArray(userCredit.userId, userIds));
|
||||
|
||||
// Create a map for quick lookup
|
||||
const userCreditMap = new Map(
|
||||
userCredits.map((record) => [record.userId, record])
|
||||
);
|
||||
|
||||
// Filter users who can receive credits
|
||||
const eligibleUserIds = userIds.filter((userId) => {
|
||||
const record = userCreditMap.get(userId);
|
||||
if (!record?.lastRefreshAt) {
|
||||
return true; // never added credits before
|
||||
}
|
||||
const last = new Date(record.lastRefreshAt);
|
||||
// different month or year means new month
|
||||
return (
|
||||
now.getMonth() !== last.getMonth() ||
|
||||
now.getFullYear() !== last.getFullYear()
|
||||
);
|
||||
});
|
||||
|
||||
if (eligibleUserIds.length === 0) {
|
||||
console.log('batchAddLifetimeMonthlyCredits, no eligible users');
|
||||
return;
|
||||
}
|
||||
|
||||
processedCount = eligibleUserIds.length;
|
||||
const expirationDate = expireDays ? addDays(now, expireDays) : undefined;
|
||||
|
||||
// Batch insert credit transactions
|
||||
const transactions = eligibleUserIds.map((userId) => ({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
type: CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY,
|
||||
amount: credits,
|
||||
remainingAmount: credits,
|
||||
description: `Lifetime monthly credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`,
|
||||
expirationDate,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
|
||||
await tx.insert(creditTransaction).values(transactions);
|
||||
|
||||
// Prepare user credit updates
|
||||
const existingUserIds = eligibleUserIds.filter((userId) =>
|
||||
userCreditMap.has(userId)
|
||||
);
|
||||
const newUserIds = eligibleUserIds.filter(
|
||||
(userId) => !userCreditMap.has(userId)
|
||||
);
|
||||
|
||||
// Insert new user credit records
|
||||
if (newUserIds.length > 0) {
|
||||
const newRecords = newUserIds.map((userId) => ({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
currentCredits: credits,
|
||||
lastRefreshAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
await tx.insert(userCredit).values(newRecords);
|
||||
}
|
||||
|
||||
// Update existing user credit records
|
||||
if (existingUserIds.length > 0) {
|
||||
await tx
|
||||
.update(userCredit)
|
||||
.set({
|
||||
currentCredits: credits,
|
||||
lastRefreshAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(inArray(userCredit.userId, existingUserIds));
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`batchAddLifetimeMonthlyCredits, ${credits} credits for ${processedCount} users, date: ${now.getFullYear()}-${now.getMonth() + 1}`
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { Inngest } from 'inngest';
|
||||
|
||||
/**
|
||||
* Create a client to send and receive events
|
||||
*
|
||||
* https://www.inngest.com/docs/getting-started/nextjs-quick-start
|
||||
*/
|
||||
export const inngest = new Inngest({ id: 'mksaas-template' });
|
@ -1,46 +0,0 @@
|
||||
import { distributeCreditsToAllUsers } from '@/credits/credits';
|
||||
import { inngest } from './client';
|
||||
|
||||
/**
|
||||
* Distribute credits to all users daily
|
||||
*
|
||||
* https://www.inngest.com/docs/guides/scheduled-functions
|
||||
*/
|
||||
export const distributeCreditsDaily = inngest.createFunction(
|
||||
{ id: 'distribute-credits-daily' },
|
||||
{ cron: 'TZ=Asia/Shanghai 0 1 * * *' },
|
||||
async ({ step }) => {
|
||||
// You should use step.run for any async or long-running logic.
|
||||
// This allows Inngest to track, retry, and visualize each step in your workflow.
|
||||
await step.run('distribute-credits-to-all-users', async () => {
|
||||
console.log('distributing credits to all users start');
|
||||
const { processedCount, errorCount } =
|
||||
await distributeCreditsToAllUsers();
|
||||
console.log(
|
||||
`distributing credits to all users end, processed: ${processedCount}, errors: ${errorCount}`
|
||||
);
|
||||
return {
|
||||
message: `credits distributed, processed: ${processedCount}, errors: ${errorCount}`,
|
||||
processedCount,
|
||||
errorCount,
|
||||
};
|
||||
});
|
||||
// you can add new steps here, for example, send email to admin
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Hello World function, for testing inngest
|
||||
*
|
||||
* https://www.inngest.com/docs/guides/scheduled-functions
|
||||
*/
|
||||
export const helloWorld = inngest.createFunction(
|
||||
{ id: 'hello-world' },
|
||||
{ event: 'test/hello.world' },
|
||||
async ({ event, step }) => {
|
||||
console.log('Hello World function start');
|
||||
await step.sleep('wait-a-moment', '1s');
|
||||
console.log('Hello World function end');
|
||||
return { message: `Hello ${event.data.email}!` };
|
||||
}
|
||||
);
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"functions": {
|
||||
"app/api/**/*": {
|
||||
"maxDuration": 60
|
||||
"maxDuration": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user