Merge remote-tracking branch 'origin/main' into cloudflare

This commit is contained in:
javayhu 2025-08-17 08:45:27 +08:00
commit 35ddf5e08e
22 changed files with 181 additions and 282 deletions

View File

@ -16,6 +16,7 @@
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"list-contacts": "tsx scripts/list-contacts.ts", "list-contacts": "tsx scripts/list-contacts.ts",
"list-users": "tsx scripts/list-users.ts",
"content": "fumadocs-mdx", "content": "fumadocs-mdx",
"email": "email dev --dir src/mail/templates --port 3333", "email": "email dev --dir src/mail/templates --port 3333",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",

24
scripts/list-users.ts Normal file
View File

@ -0,0 +1,24 @@
import dotenv from 'dotenv';
import { getDb } from '../src/db/index.js';
import { user } from '../src/db/schema.js';
dotenv.config();
export default async function listUsers() {
const db = await getDb();
try {
const users = await db.select({ email: user.email }).from(user);
// Extract emails from users
const emails: string[] = users.map((user) => user.email);
console.log(`Total users: ${emails.length}`);
// Output all emails joined with comma
console.log(emails.join(', '));
} catch (error) {
console.error('Error fetching users:', error);
}
}
listUsers();

View File

@ -1,19 +1,16 @@
'use server'; 'use server';
import { userActionClient } from '@/lib/safe-action';
import { isSubscribed } from '@/newsletter'; import { isSubscribed } from '@/newsletter';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Newsletter schema for validation // Newsletter schema for validation
const newsletterSchema = z.object({ const newsletterSchema = z.object({
email: z.email({ error: 'Please enter a valid email address' }), email: z.email({ error: 'Please enter a valid email address' }),
}); });
// Create a safe action to check if a user is subscribed to the newsletter // Create a safe action to check if a user is subscribed to the newsletter
export const checkNewsletterStatusAction = actionClient export const checkNewsletterStatusAction = userActionClient
.schema(newsletterSchema) .schema(newsletterSchema)
.action(async ({ parsedInput: { email } }) => { .action(async ({ parsedInput: { email } }) => {
try { try {

View File

@ -2,29 +2,21 @@
import { getWebContentAnalysisCost } from '@/ai/text/utils/web-content-analyzer-config'; import { getWebContentAnalysisCost } from '@/ai/text/utils/web-content-analyzer-config';
import { getUserCredits, hasEnoughCredits } from '@/credits/credits'; import { getUserCredits, hasEnoughCredits } from '@/credits/credits';
import { getSession } from '@/lib/server'; import type { User } from '@/lib/auth-types';
import { createSafeActionClient } from 'next-safe-action'; import { userActionClient } from '@/lib/safe-action';
const actionClient = createSafeActionClient();
/** /**
* Check if user has enough credits for web content analysis * Check if user has enough credits for web content analysis
*/ */
export const checkWebContentAnalysisCreditsAction = actionClient.action( export const checkWebContentAnalysisCreditsAction = userActionClient.action(
async () => { async ({ ctx }) => {
const session = await getSession(); const currentUser = (ctx as { user: User }).user;
if (!session) {
console.warn(
'unauthorized request to check web content analysis credits'
);
return { success: false, error: 'Unauthorized' };
}
try { try {
const requiredCredits = getWebContentAnalysisCost(); const requiredCredits = getWebContentAnalysisCost();
const currentCredits = await getUserCredits(session.user.id); const currentCredits = await getUserCredits(currentUser.id);
const hasCredits = await hasEnoughCredits({ const hasCredits = await hasEnoughCredits({
userId: session.user.id, userId: currentUser.id,
requiredCredits, requiredCredits,
}); });

View File

@ -1,12 +1,10 @@
'use server'; 'use server';
import { consumeCredits } from '@/credits/credits'; import { consumeCredits } from '@/credits/credits';
import { getSession } from '@/lib/server'; import type { User } from '@/lib/auth-types';
import { createSafeActionClient } from 'next-safe-action'; import { userActionClient } from '@/lib/safe-action';
import { z } from 'zod'; import { z } from 'zod';
const actionClient = createSafeActionClient();
// consume credits schema // consume credits schema
const consumeSchema = z.object({ const consumeSchema = z.object({
amount: z.number().min(1), amount: z.number().min(1),
@ -16,21 +14,17 @@ const consumeSchema = z.object({
/** /**
* Consume credits * Consume credits
*/ */
export const consumeCreditsAction = actionClient export const consumeCreditsAction = userActionClient
.schema(consumeSchema) .schema(consumeSchema)
.action(async ({ parsedInput }) => { .action(async ({ parsedInput, ctx }) => {
const session = await getSession(); const { amount, description } = parsedInput;
if (!session) { const currentUser = (ctx as { user: User }).user;
console.warn('unauthorized request to consume credits');
return { success: false, error: 'Unauthorized' };
}
try { try {
await consumeCredits({ await consumeCredits({
userId: session.user.id, userId: currentUser.id,
amount: parsedInput.amount, amount,
description: description: description || `Consume credits: ${amount}`,
parsedInput.description || `Consume credits: ${parsedInput.amount}`,
}); });
return { success: true }; return { success: true };
} catch (error) { } catch (error) {

View File

@ -1,20 +1,17 @@
'use server'; 'use server';
import { websiteConfig } from '@/config/website'; import { websiteConfig } from '@/config/website';
import type { User } from '@/lib/auth-types';
import { findPlanByPlanId } from '@/lib/price-plan'; import { findPlanByPlanId } from '@/lib/price-plan';
import { getSession } from '@/lib/server'; import { userActionClient } from '@/lib/safe-action';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { createCheckout } from '@/payment'; import { createCheckout } from '@/payment';
import type { CreateCheckoutParams } from '@/payment/types'; import type { CreateCheckoutParams } from '@/payment/types';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { getLocale } from 'next-intl/server'; import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Checkout schema for validation // Checkout schema for validation
// metadata is optional, and may contain referral information if you need // metadata is optional, and may contain referral information if you need
const checkoutSchema = z.object({ const checkoutSchema = z.object({
@ -27,33 +24,11 @@ const checkoutSchema = z.object({
/** /**
* Create a checkout session for a price plan * Create a checkout session for a price plan
*/ */
export const createCheckoutAction = actionClient export const createCheckoutAction = userActionClient
.schema(checkoutSchema) .schema(checkoutSchema)
.action(async ({ parsedInput }) => { .action(async ({ parsedInput, ctx }) => {
const { userId, planId, priceId, metadata } = parsedInput; const { planId, priceId, metadata } = parsedInput;
const currentUser = (ctx as { user: User }).user;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to create checkout session for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Only allow users to create their own checkout session
if (session.user.id !== userId) {
console.warn(
`current user ${session.user.id} is not authorized to create checkout session for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
};
}
try { try {
// Get the current locale from the request // Get the current locale from the request
@ -71,8 +46,8 @@ export const createCheckoutAction = actionClient
// Add user id to metadata, so we can get it in the webhook event // Add user id to metadata, so we can get it in the webhook event
const customMetadata: Record<string, string> = { const customMetadata: Record<string, string> = {
...metadata, ...metadata,
userId: session.user.id, userId: currentUser.id,
userName: session.user.name, userName: currentUser.name,
}; };
// https://datafa.st/docs/stripe-checkout-api // https://datafa.st/docs/stripe-checkout-api
@ -94,7 +69,7 @@ export const createCheckoutAction = actionClient
const params: CreateCheckoutParams = { const params: CreateCheckoutParams = {
planId, planId,
priceId, priceId,
customerEmail: session.user.email, customerEmail: currentUser.email,
metadata: customMetadata, metadata: customMetadata,
successUrl, successUrl,
cancelUrl, cancelUrl,

View File

@ -2,19 +2,16 @@
import { websiteConfig } from '@/config/website'; import { websiteConfig } from '@/config/website';
import { getCreditPackageById } from '@/credits/server'; import { getCreditPackageById } from '@/credits/server';
import { getSession } from '@/lib/server'; import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { createCreditCheckout } from '@/payment'; import { createCreditCheckout } from '@/payment';
import type { CreateCreditCheckoutParams } from '@/payment/types'; import type { CreateCreditCheckoutParams } from '@/payment/types';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { getLocale } from 'next-intl/server'; import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Credit checkout schema for validation // Credit checkout schema for validation
// metadata is optional, and may contain referral information if you need // metadata is optional, and may contain referral information if you need
const creditCheckoutSchema = z.object({ const creditCheckoutSchema = z.object({
@ -27,33 +24,11 @@ const creditCheckoutSchema = z.object({
/** /**
* Create a checkout session for a credit package * Create a checkout session for a credit package
*/ */
export const createCreditCheckoutSession = actionClient export const createCreditCheckoutSession = userActionClient
.schema(creditCheckoutSchema) .schema(creditCheckoutSchema)
.action(async ({ parsedInput }) => { .action(async ({ parsedInput, ctx }) => {
const { userId, packageId, priceId, metadata } = parsedInput; const { packageId, priceId, metadata } = parsedInput;
const currentUser = (ctx as { user: User }).user;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to create credit checkout session for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Only allow users to create their own checkout session
if (session.user.id !== userId) {
console.warn(
`current user ${session.user.id} is not authorized to create credit checkout session for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
};
}
try { try {
// Get the current locale from the request // Get the current locale from the request
@ -74,8 +49,8 @@ export const createCreditCheckoutSession = actionClient
type: 'credit_purchase', type: 'credit_purchase',
packageId, packageId,
credits: creditPackage.credits.toString(), credits: creditPackage.credits.toString(),
userId: session.user.id, userId: currentUser.id,
userName: session.user.name, userName: currentUser.name,
}; };
// https://datafa.st/docs/stripe-checkout-api // https://datafa.st/docs/stripe-checkout-api
@ -98,7 +73,7 @@ export const createCreditCheckoutSession = actionClient
const params: CreateCreditCheckoutParams = { const params: CreateCreditCheckoutParams = {
packageId, packageId,
priceId, priceId,
customerEmail: session.user.email, customerEmail: currentUser.email,
metadata: customMetadata, metadata: customMetadata,
successUrl, successUrl,
cancelUrl, cancelUrl,

View File

@ -2,18 +2,15 @@
import { getDb } from '@/db'; import { getDb } from '@/db';
import { user } from '@/db/schema'; import { user } from '@/db/schema';
import { getSession } from '@/lib/server'; import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { getUrlWithLocale } from '@/lib/urls/urls'; import { getUrlWithLocale } from '@/lib/urls/urls';
import { createCustomerPortal } from '@/payment'; import { createCustomerPortal } from '@/payment';
import type { CreatePortalParams } from '@/payment/types'; import type { CreatePortalParams } from '@/payment/types';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { getLocale } from 'next-intl/server'; import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Portal schema for validation // Portal schema for validation
const portalSchema = z.object({ const portalSchema = z.object({
userId: z.string().min(1, { error: 'User ID is required' }), userId: z.string().min(1, { error: 'User ID is required' }),
@ -26,33 +23,11 @@ const portalSchema = z.object({
/** /**
* Create a customer portal session * Create a customer portal session
*/ */
export const createPortalAction = actionClient export const createPortalAction = userActionClient
.schema(portalSchema) .schema(portalSchema)
.action(async ({ parsedInput }) => { .action(async ({ parsedInput, ctx }) => {
const { userId, returnUrl } = parsedInput; const { returnUrl } = parsedInput;
const currentUser = (ctx as { user: User }).user;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to create portal session for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Only allow users to create their own portal session
if (session.user.id !== userId) {
console.warn(
`current user ${session.user.id} is not authorized to create portal session for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
};
}
try { try {
// Get the user's customer ID from the database // Get the user's customer ID from the database
@ -60,11 +35,11 @@ export const createPortalAction = actionClient
const customerResult = await db const customerResult = await db
.select({ customerId: user.customerId }) .select({ customerId: user.customerId })
.from(user) .from(user)
.where(eq(user.id, session.user.id)) .where(eq(user.id, currentUser.id))
.limit(1); .limit(1);
if (customerResult.length <= 0 || !customerResult[0].customerId) { if (customerResult.length <= 0 || !customerResult[0].customerId) {
console.error(`No customer found for user ${session.user.id}`); console.error(`No customer found for user ${currentUser.id}`);
return { return {
success: false, success: false,
error: 'No customer found for user', error: 'No customer found for user',

View File

@ -1,13 +1,10 @@
'use server'; 'use server';
import { getSession } from '@/lib/server'; import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { getSubscriptions } from '@/payment'; import { getSubscriptions } from '@/payment';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Input schema // Input schema
const schema = z.object({ const schema = z.object({
userId: z.string().min(1, { error: 'User ID is required' }), userId: z.string().min(1, { error: 'User ID is required' }),
@ -19,33 +16,10 @@ const schema = z.object({
* If the user has multiple subscriptions, * If the user has multiple subscriptions,
* it returns the most recent active or trialing one * it returns the most recent active or trialing one
*/ */
export const getActiveSubscriptionAction = actionClient export const getActiveSubscriptionAction = userActionClient
.schema(schema) .schema(schema)
.action(async ({ parsedInput }) => { .action(async ({ ctx }) => {
const { userId } = parsedInput; const currentUser = (ctx as { user: User }).user;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to get active subscription for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Only allow users to check their own status unless they're admins
if (session.user.id !== userId && session.user.role !== 'admin') {
console.warn(
`current user ${session.user.id} is not authorized to get active subscription for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
};
}
// Check if Stripe environment variables are configured // Check if Stripe environment variables are configured
const stripeSecretKey = process.env.STRIPE_SECRET_KEY; const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
@ -62,7 +36,7 @@ export const getActiveSubscriptionAction = actionClient
try { try {
// Find the user's most recent active subscription // Find the user's most recent active subscription
const subscriptions = await getSubscriptions({ const subscriptions = await getSubscriptions({
userId: session.user.id, userId: currentUser.id,
}); });
// console.log('get user subscriptions:', subscriptions); // console.log('get user subscriptions:', subscriptions);
@ -76,16 +50,16 @@ export const getActiveSubscriptionAction = actionClient
// If found, use it // If found, use it
if (activeSubscription) { if (activeSubscription) {
console.log('find active subscription for userId:', session.user.id); console.log('find active subscription for userId:', currentUser.id);
subscriptionData = activeSubscription; subscriptionData = activeSubscription;
} else { } else {
console.log( console.log(
'no active subscription found for userId:', 'no active subscription found for userId:',
session.user.id currentUser.id
); );
} }
} else { } else {
console.log('no subscriptions found for userId:', session.user.id); console.log('no subscriptions found for userId:', currentUser.id);
} }
return { return {

View File

@ -1,21 +1,16 @@
'use server'; 'use server';
import { getUserCredits } from '@/credits/credits'; import { getUserCredits } from '@/credits/credits';
import { getSession } from '@/lib/server'; import type { User } from '@/lib/auth-types';
import { createSafeActionClient } from 'next-safe-action'; import { userActionClient } from '@/lib/safe-action';
const actionClient = createSafeActionClient();
/** /**
* Get current user's credits * Get current user's credits
*/ */
export const getCreditBalanceAction = actionClient.action(async () => { export const getCreditBalanceAction = userActionClient.action(
const session = await getSession(); async ({ ctx }) => {
if (!session) { const currentUser = (ctx as { user: User }).user;
console.warn('unauthorized request to get credit balance'); const credits = await getUserCredits(currentUser.id);
return { success: false, error: 'Unauthorized' }; return { success: true, credits };
} }
);
const credits = await getUserCredits(session.user.id);
return { success: true, credits };
});

View File

@ -3,34 +3,23 @@
import { CREDIT_TRANSACTION_TYPE } from '@/credits/types'; import { CREDIT_TRANSACTION_TYPE } from '@/credits/types';
import { getDb } from '@/db'; import { getDb } from '@/db';
import { creditTransaction } from '@/db/schema'; import { creditTransaction } from '@/db/schema';
import { getSession } from '@/lib/server'; import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import { and, eq, gte, isNotNull, lte, sql, sum } from 'drizzle-orm'; import { and, eq, gte, isNotNull, lte, sql, sum } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
const CREDITS_EXPIRATION_DAYS = 31; const CREDITS_EXPIRATION_DAYS = 31;
const CREDITS_MONTHLY_DAYS = 31; const CREDITS_MONTHLY_DAYS = 31;
// Create a safe action client
const actionClient = createSafeActionClient();
/** /**
* Get credit statistics for a user * Get credit statistics for a user
*/ */
export const getCreditStatsAction = actionClient.action(async () => { export const getCreditStatsAction = userActionClient.action(async ({ ctx }) => {
try { try {
const session = await getSession(); const currentUser = (ctx as { user: User }).user;
if (!session) { const userId = currentUser.id;
console.warn('unauthorized request to get credit stats');
return {
success: false,
error: 'Unauthorized',
};
}
const db = await getDb(); const db = await getDb();
const userId = session.user.id;
// Get credits expiring in the next CREDITS_EXPIRATION_DAYS days // Get credits expiring in the next CREDITS_EXPIRATION_DAYS days
const expirationDaysFromNow = addDays(new Date(), CREDITS_EXPIRATION_DAYS); const expirationDaysFromNow = addDays(new Date(), CREDITS_EXPIRATION_DAYS);
const expiringCredits = await db const expiringCredits = await db

View File

@ -2,14 +2,11 @@
import { getDb } from '@/db'; import { getDb } from '@/db';
import { creditTransaction } from '@/db/schema'; import { creditTransaction } from '@/db/schema';
import { getSession } from '@/lib/server'; import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { and, asc, desc, eq, ilike, or, sql } from 'drizzle-orm'; import { and, asc, desc, eq, ilike, or, sql } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Define the schema for getCreditTransactions parameters // Define the schema for getCreditTransactions parameters
const getCreditTransactionsSchema = z.object({ const getCreditTransactionsSchema = z.object({
pageIndex: z.number().min(0).default(0), pageIndex: z.number().min(0).default(0),
@ -40,23 +37,17 @@ const sortFieldMap = {
} as const; } as const;
// Create a safe action for getting credit transactions // Create a safe action for getting credit transactions
export const getCreditTransactionsAction = actionClient export const getCreditTransactionsAction = userActionClient
.schema(getCreditTransactionsSchema) .schema(getCreditTransactionsSchema)
.action(async ({ parsedInput }) => { .action(async ({ parsedInput, ctx }) => {
try { try {
const session = await getSession();
if (!session) {
return {
success: false,
error: 'Unauthorized',
};
}
const { pageIndex, pageSize, search, sorting } = parsedInput; const { pageIndex, pageSize, search, sorting } = parsedInput;
const currentUser = (ctx as { user: User }).user;
// search by type, amount, paymentId, description, and restrict to current user // search by type, amount, paymentId, description, and restrict to current user
const where = search const where = search
? and( ? and(
eq(creditTransaction.userId, session.user.id), eq(creditTransaction.userId, currentUser.id),
or( or(
ilike(creditTransaction.type, `%${search}%`), ilike(creditTransaction.type, `%${search}%`),
ilike(creditTransaction.amount, `%${search}%`), ilike(creditTransaction.amount, `%${search}%`),
@ -65,7 +56,7 @@ export const getCreditTransactionsAction = actionClient
ilike(creditTransaction.description, `%${search}%`) ilike(creditTransaction.description, `%${search}%`)
) )
) )
: eq(creditTransaction.userId, session.user.id); : eq(creditTransaction.userId, currentUser.id);
const offset = pageIndex * pageSize; const offset = pageIndex * pageSize;

View File

@ -2,16 +2,13 @@
import { getDb } from '@/db'; import { getDb } from '@/db';
import { payment } from '@/db/schema'; import { payment } from '@/db/schema';
import type { User } from '@/lib/auth-types';
import { findPlanByPriceId, getAllPricePlans } from '@/lib/price-plan'; import { findPlanByPriceId, getAllPricePlans } from '@/lib/price-plan';
import { getSession } from '@/lib/server'; import { userActionClient } from '@/lib/safe-action';
import { PaymentTypes } from '@/payment/types'; import { PaymentTypes } from '@/payment/types';
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Input schema // Input schema
const schema = z.object({ const schema = z.object({
userId: z.string().min(1, { error: 'User ID is required' }), userId: z.string().min(1, { error: 'User ID is required' }),
@ -25,33 +22,11 @@ const schema = z.object({
* in order to do this, you have to update the logic to check the lifetime status, * in order to do this, you have to update the logic to check the lifetime status,
* for example, just check the planId is `lifetime` or not. * for example, just check the planId is `lifetime` or not.
*/ */
export const getLifetimeStatusAction = actionClient export const getLifetimeStatusAction = userActionClient
.schema(schema) .schema(schema)
.action(async ({ parsedInput }) => { .action(async ({ ctx }) => {
const { userId } = parsedInput; const currentUser = (ctx as { user: User }).user;
const userId = currentUser.id;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to get lifetime status for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Only allow users to check their own status unless they're admins
if (session.user.id !== userId && session.user.role !== 'admin') {
console.warn(
`current user ${session.user.id} is not authorized to get lifetime status for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
};
}
try { try {
// Get lifetime plans // Get lifetime plans

View File

@ -3,13 +3,10 @@
import { getDb } from '@/db'; import { getDb } from '@/db';
import { user } from '@/db/schema'; import { user } from '@/db/schema';
import { isDemoWebsite } from '@/lib/demo'; import { isDemoWebsite } from '@/lib/demo';
import { adminActionClient } from '@/lib/safe-action';
import { asc, desc, ilike, or, sql } from 'drizzle-orm'; import { asc, desc, ilike, or, sql } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Define the schema for getUsers parameters // Define the schema for getUsers parameters
const getUsersSchema = z.object({ const getUsersSchema = z.object({
pageIndex: z.number().min(0).default(0), pageIndex: z.number().min(0).default(0),
@ -39,7 +36,7 @@ const sortFieldMap = {
} as const; } as const;
// Create a safe action for getting users // Create a safe action for getting users
export const getUsersAction = actionClient export const getUsersAction = adminActionClient
.schema(getUsersSchema) .schema(getUsersSchema)
.action(async ({ parsedInput }) => { .action(async ({ parsedInput }) => {
try { try {

View File

@ -1,14 +1,11 @@
'use server'; 'use server';
import { websiteConfig } from '@/config/website'; import { websiteConfig } from '@/config/website';
import { actionClient } from '@/lib/safe-action';
import { sendEmail } from '@/mail'; import { sendEmail } from '@/mail';
import { getLocale } from 'next-intl/server'; import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
/** /**
* DOC: When using Zod for validation, how can I localize error messages? * DOC: When using Zod for validation, how can I localize error messages?
* https://next-intl.dev/docs/environments/actions-metadata-route-handlers#server-actions * https://next-intl.dev/docs/environments/actions-metadata-route-handlers#server-actions

View File

@ -1,14 +1,11 @@
'use server'; 'use server';
import { actionClient } from '@/lib/safe-action';
import { sendEmail } from '@/mail'; import { sendEmail } from '@/mail';
import { subscribe } from '@/newsletter'; import { subscribe } from '@/newsletter';
import { getLocale } from 'next-intl/server'; import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Newsletter schema for validation // Newsletter schema for validation
const newsletterSchema = z.object({ const newsletterSchema = z.object({
email: z.email({ error: 'Please enter a valid email address' }), email: z.email({ error: 'Please enter a valid email address' }),

View File

@ -1,30 +1,18 @@
'use server'; 'use server';
import { getSession } from '@/lib/server'; import { userActionClient } from '@/lib/safe-action';
import { unsubscribe } from '@/newsletter'; import { unsubscribe } from '@/newsletter';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Newsletter schema for validation // Newsletter schema for validation
const newsletterSchema = z.object({ const newsletterSchema = z.object({
email: z.email({ error: 'Please enter a valid email address' }), email: z.email({ error: 'Please enter a valid email address' }),
}); });
// Create a safe action for newsletter unsubscription // Create a safe action for newsletter unsubscription
export const unsubscribeNewsletterAction = actionClient export const unsubscribeNewsletterAction = userActionClient
.schema(newsletterSchema) .schema(newsletterSchema)
.action(async ({ parsedInput: { email } }) => { .action(async ({ parsedInput: { email } }) => {
const session = await getSession();
if (!session) {
return {
success: false,
error: 'Unauthorized',
};
}
try { try {
const unsubscribed = await unsubscribe(email); const unsubscribed = await unsubscribe(email);

View File

@ -1,12 +1,9 @@
'use server'; 'use server';
import { validateTurnstileToken } from '@/lib/captcha'; import { validateTurnstileToken } from '@/lib/captcha';
import { createSafeActionClient } from 'next-safe-action'; import { actionClient } from '@/lib/safe-action';
import { z } from 'zod'; import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Captcha validation schema // Captcha validation schema
const captchaSchema = z.object({ const captchaSchema = z.object({
captchaToken: z.string().min(1, { error: 'Captcha token is required' }), captchaToken: z.string().min(1, { error: 'Captcha token is required' }),

View File

@ -95,6 +95,10 @@ export const LoginForm = ({
const onSubmit = async (values: z.infer<typeof LoginSchema>) => { const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
// Validate captcha token if turnstile is enabled and site key is available // Validate captcha token if turnstile is enabled and site key is available
if (captchaConfigured && values.captchaToken) { if (captchaConfigured && values.captchaToken) {
setIsPending(true);
setError('');
setSuccess('');
const captchaResult = await validateCaptchaAction({ const captchaResult = await validateCaptchaAction({
captchaToken: values.captchaToken, captchaToken: values.captchaToken,
}); });

View File

@ -94,6 +94,10 @@ export const RegisterForm = ({
const onSubmit = async (values: z.infer<typeof RegisterSchema>) => { const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
// Validate captcha token if turnstile is enabled and site key is available // Validate captcha token if turnstile is enabled and site key is available
if (captchaConfigured && values.captchaToken) { if (captchaConfigured && values.captchaToken) {
setIsPending(true);
setError('');
setSuccess('');
const captchaResult = await validateCaptchaAction({ const captchaResult = await validateCaptchaAction({
captchaToken: values.captchaToken, captchaToken: values.captchaToken,
}); });
@ -119,13 +123,13 @@ export const RegisterForm = ({
}, },
{ {
onRequest: (ctx) => { onRequest: (ctx) => {
console.log('register, request:', ctx.url); // console.log('register, request:', ctx.url);
setIsPending(true); setIsPending(true);
setError(''); setError('');
setSuccess(''); setSuccess('');
}, },
onResponse: (ctx) => { onResponse: (ctx) => {
console.log('register, response:', ctx.response); // console.log('register, response:', ctx.response);
setIsPending(false); setIsPending(false);
}, },
onSuccess: (ctx) => { onSuccess: (ctx) => {

57
src/lib/safe-action.ts Normal file
View File

@ -0,0 +1,57 @@
import { createSafeActionClient } from 'next-safe-action';
import type { User } from './auth-types';
import { isDemoWebsite } from './demo';
import { getSession } from './server';
// -----------------------------------------------------------------------------
// 1. Base action client put global error handling / metadata here if needed
// -----------------------------------------------------------------------------
export const actionClient = createSafeActionClient({
handleServerError: (e) => {
if (e instanceof Error) {
return {
success: false,
error: e.message,
};
}
return {
success: false,
error: 'Something went wrong while executing the action',
};
},
});
// -----------------------------------------------------------------------------
// 2. Auth-guarded client
// -----------------------------------------------------------------------------
export const userActionClient = actionClient.use(async ({ next }) => {
const session = await getSession();
if (!session?.user) {
return {
success: false,
error: 'Unauthorized',
};
}
return next({ ctx: { user: session.user } });
});
// -----------------------------------------------------------------------------
// 3. Admin-only client (extends auth client)
// -----------------------------------------------------------------------------
export const adminActionClient = userActionClient.use(async ({ next, ctx }) => {
const user = (ctx as { user: User }).user;
const isDemo = isDemoWebsite();
const isAdmin = user.role === 'admin';
// If this is a demo website and user is not an admin, allow the request
if (!isAdmin && !isDemo) {
return {
success: false,
error: 'Unauthorized',
};
}
return next({ ctx });
});

View File

@ -76,7 +76,7 @@ export const useCreditsStore = create<CreditsState>((set, get) => ({
try { try {
const result = await getCreditBalanceAction(); const result = await getCreditBalanceAction();
if (result?.data?.success) { if (result?.data?.success && result.data.credits !== undefined) {
const newBalance = result.data.credits || 0; const newBalance = result.data.credits || 0;
console.log('fetchCredits, set new balance', newBalance); console.log('fetchCredits, set new balance', newBalance);
set({ set({
@ -88,7 +88,8 @@ export const useCreditsStore = create<CreditsState>((set, get) => ({
} else { } else {
console.warn('fetchCredits, failed to fetch credit balance', result); console.warn('fetchCredits, failed to fetch credit balance', result);
set({ set({
error: result?.data?.error || 'Failed to fetch credit balance', error:
(result?.data as any)?.error || 'Failed to fetch credit balance',
isLoading: false, isLoading: false,
}); });
} }