From d1928575b3412fab3cb390efe015eb5c5f486c21 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 16 Aug 2025 23:00:21 +0800 Subject: [PATCH] refactor: replace createSafeActionClient with userActionClient for improved session handling across multiple actions --- src/actions/check-newsletter-status.ts | 7 +-- .../check-web-content-analysis-credits.ts | 22 +++---- src/actions/consume-credits.ts | 24 +++----- src/actions/create-checkout-session.ts | 43 +++----------- src/actions/create-credit-checkout-session.ts | 43 +++----------- src/actions/create-customer-portal-session.ts | 41 +++---------- src/actions/get-active-subscription.ts | 44 +++----------- src/actions/get-credit-balance.ts | 21 +++---- src/actions/get-credit-stats.ts | 21 ++----- src/actions/get-credit-transactions.ts | 23 +++----- src/actions/get-lifetime-status.ts | 37 ++---------- src/actions/get-users.ts | 16 +----- src/actions/send-message.ts | 5 +- src/actions/subscribe-newsletter.ts | 5 +- src/actions/unsubscribe-newsletter.ts | 16 +----- src/actions/validate-captcha.ts | 5 +- src/lib/safe-action.ts | 57 +++++++++++++++++++ 17 files changed, 143 insertions(+), 287 deletions(-) create mode 100644 src/lib/safe-action.ts diff --git a/src/actions/check-newsletter-status.ts b/src/actions/check-newsletter-status.ts index d40952d..5eb05af 100644 --- a/src/actions/check-newsletter-status.ts +++ b/src/actions/check-newsletter-status.ts @@ -1,19 +1,16 @@ 'use server'; +import { userActionClient } from '@/lib/safe-action'; import { isSubscribed } from '@/newsletter'; -import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Newsletter schema for validation const newsletterSchema = z.object({ email: z.email({ error: 'Please enter a valid email address' }), }); // Create a safe action to check if a user is subscribed to the newsletter -export const checkNewsletterStatusAction = actionClient +export const checkNewsletterStatusAction = userActionClient .schema(newsletterSchema) .action(async ({ parsedInput: { email } }) => { try { diff --git a/src/actions/check-web-content-analysis-credits.ts b/src/actions/check-web-content-analysis-credits.ts index 5d253da..3ca2c7f 100644 --- a/src/actions/check-web-content-analysis-credits.ts +++ b/src/actions/check-web-content-analysis-credits.ts @@ -2,29 +2,21 @@ import { getWebContentAnalysisCost } from '@/ai/text/utils/web-content-analyzer-config'; import { getUserCredits, hasEnoughCredits } from '@/credits/credits'; -import { getSession } from '@/lib/server'; -import { createSafeActionClient } from 'next-safe-action'; - -const actionClient = createSafeActionClient(); +import type { User } from '@/lib/auth-types'; +import { userActionClient } from '@/lib/safe-action'; /** * Check if user has enough credits for web content analysis */ -export const checkWebContentAnalysisCreditsAction = actionClient.action( - async () => { - const session = await getSession(); - if (!session) { - console.warn( - 'unauthorized request to check web content analysis credits' - ); - return { success: false, error: 'Unauthorized' }; - } +export const checkWebContentAnalysisCreditsAction = userActionClient.action( + async ({ ctx }) => { + const currentUser = (ctx as { user: User }).user; try { const requiredCredits = getWebContentAnalysisCost(); - const currentCredits = await getUserCredits(session.user.id); + const currentCredits = await getUserCredits(currentUser.id); const hasCredits = await hasEnoughCredits({ - userId: session.user.id, + userId: currentUser.id, requiredCredits, }); diff --git a/src/actions/consume-credits.ts b/src/actions/consume-credits.ts index b9a7210..58b7ea2 100644 --- a/src/actions/consume-credits.ts +++ b/src/actions/consume-credits.ts @@ -1,12 +1,10 @@ 'use server'; import { consumeCredits } from '@/credits/credits'; -import { getSession } from '@/lib/server'; -import { createSafeActionClient } from 'next-safe-action'; +import type { User } from '@/lib/auth-types'; +import { userActionClient } from '@/lib/safe-action'; import { z } from 'zod'; -const actionClient = createSafeActionClient(); - // consume credits schema const consumeSchema = z.object({ amount: z.number().min(1), @@ -16,21 +14,17 @@ const consumeSchema = z.object({ /** * Consume credits */ -export const consumeCreditsAction = actionClient +export const consumeCreditsAction = userActionClient .schema(consumeSchema) - .action(async ({ parsedInput }) => { - const session = await getSession(); - if (!session) { - console.warn('unauthorized request to consume credits'); - return { success: false, error: 'Unauthorized' }; - } + .action(async ({ parsedInput, ctx }) => { + const { amount, description } = parsedInput; + const currentUser = (ctx as { user: User }).user; try { await consumeCredits({ - userId: session.user.id, - amount: parsedInput.amount, - description: - parsedInput.description || `Consume credits: ${parsedInput.amount}`, + userId: currentUser.id, + amount, + description: description || `Consume credits: ${amount}`, }); return { success: true }; } catch (error) { diff --git a/src/actions/create-checkout-session.ts b/src/actions/create-checkout-session.ts index 5c0ac29..8aa902d 100644 --- a/src/actions/create-checkout-session.ts +++ b/src/actions/create-checkout-session.ts @@ -1,20 +1,17 @@ 'use server'; import { websiteConfig } from '@/config/website'; +import type { User } from '@/lib/auth-types'; import { findPlanByPlanId } from '@/lib/price-plan'; -import { getSession } from '@/lib/server'; +import { userActionClient } from '@/lib/safe-action'; import { getUrlWithLocale } from '@/lib/urls/urls'; import { createCheckout } from '@/payment'; import type { CreateCheckoutParams } from '@/payment/types'; import { Routes } from '@/routes'; import { getLocale } from 'next-intl/server'; -import { createSafeActionClient } from 'next-safe-action'; import { cookies } from 'next/headers'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Checkout schema for validation // metadata is optional, and may contain referral information if you need const checkoutSchema = z.object({ @@ -27,33 +24,11 @@ const checkoutSchema = z.object({ /** * Create a checkout session for a price plan */ -export const createCheckoutAction = actionClient +export const createCheckoutAction = userActionClient .schema(checkoutSchema) - .action(async ({ parsedInput }) => { - const { userId, planId, priceId, metadata } = parsedInput; - - // 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', - }; - } + .action(async ({ parsedInput, ctx }) => { + const { planId, priceId, metadata } = parsedInput; + const currentUser = (ctx as { user: User }).user; try { // 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 const customMetadata: Record = { ...metadata, - userId: session.user.id, - userName: session.user.name, + userId: currentUser.id, + userName: currentUser.name, }; // https://datafa.st/docs/stripe-checkout-api @@ -94,7 +69,7 @@ export const createCheckoutAction = actionClient const params: CreateCheckoutParams = { planId, priceId, - customerEmail: session.user.email, + customerEmail: currentUser.email, metadata: customMetadata, successUrl, cancelUrl, diff --git a/src/actions/create-credit-checkout-session.ts b/src/actions/create-credit-checkout-session.ts index 9e8ff7f..218eb59 100644 --- a/src/actions/create-credit-checkout-session.ts +++ b/src/actions/create-credit-checkout-session.ts @@ -2,19 +2,16 @@ import { websiteConfig } from '@/config/website'; 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 { createCreditCheckout } from '@/payment'; import type { CreateCreditCheckoutParams } from '@/payment/types'; import { Routes } from '@/routes'; import { getLocale } from 'next-intl/server'; -import { createSafeActionClient } from 'next-safe-action'; import { cookies } from 'next/headers'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Credit checkout schema for validation // metadata is optional, and may contain referral information if you need const creditCheckoutSchema = z.object({ @@ -27,33 +24,11 @@ const creditCheckoutSchema = z.object({ /** * Create a checkout session for a credit package */ -export const createCreditCheckoutSession = actionClient +export const createCreditCheckoutSession = userActionClient .schema(creditCheckoutSchema) - .action(async ({ parsedInput }) => { - const { userId, packageId, priceId, metadata } = parsedInput; - - // 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', - }; - } + .action(async ({ parsedInput, ctx }) => { + const { packageId, priceId, metadata } = parsedInput; + const currentUser = (ctx as { user: User }).user; try { // Get the current locale from the request @@ -74,8 +49,8 @@ export const createCreditCheckoutSession = actionClient type: 'credit_purchase', packageId, credits: creditPackage.credits.toString(), - userId: session.user.id, - userName: session.user.name, + userId: currentUser.id, + userName: currentUser.name, }; // https://datafa.st/docs/stripe-checkout-api @@ -98,7 +73,7 @@ export const createCreditCheckoutSession = actionClient const params: CreateCreditCheckoutParams = { packageId, priceId, - customerEmail: session.user.email, + customerEmail: currentUser.email, metadata: customMetadata, successUrl, cancelUrl, diff --git a/src/actions/create-customer-portal-session.ts b/src/actions/create-customer-portal-session.ts index feeb7f3..3fd0be8 100644 --- a/src/actions/create-customer-portal-session.ts +++ b/src/actions/create-customer-portal-session.ts @@ -2,18 +2,15 @@ import { getDb } from '@/db'; 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 { createCustomerPortal } from '@/payment'; import type { CreatePortalParams } from '@/payment/types'; import { eq } from 'drizzle-orm'; import { getLocale } from 'next-intl/server'; -import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Portal schema for validation const portalSchema = z.object({ userId: z.string().min(1, { error: 'User ID is required' }), @@ -26,33 +23,11 @@ const portalSchema = z.object({ /** * Create a customer portal session */ -export const createPortalAction = actionClient +export const createPortalAction = userActionClient .schema(portalSchema) - .action(async ({ parsedInput }) => { - const { userId, returnUrl } = parsedInput; - - // 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', - }; - } + .action(async ({ parsedInput, ctx }) => { + const { returnUrl } = parsedInput; + const currentUser = (ctx as { user: User }).user; try { // Get the user's customer ID from the database @@ -60,11 +35,11 @@ export const createPortalAction = actionClient const customerResult = await db .select({ customerId: user.customerId }) .from(user) - .where(eq(user.id, session.user.id)) + .where(eq(user.id, currentUser.id)) .limit(1); 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 { success: false, error: 'No customer found for user', diff --git a/src/actions/get-active-subscription.ts b/src/actions/get-active-subscription.ts index c1d87dc..5b1a663 100644 --- a/src/actions/get-active-subscription.ts +++ b/src/actions/get-active-subscription.ts @@ -1,13 +1,10 @@ '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 { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Input schema const schema = z.object({ userId: z.string().min(1, { error: 'User ID is required' }), @@ -19,33 +16,10 @@ const schema = z.object({ * If the user has multiple subscriptions, * it returns the most recent active or trialing one */ -export const getActiveSubscriptionAction = actionClient +export const getActiveSubscriptionAction = userActionClient .schema(schema) - .action(async ({ parsedInput }) => { - const { userId } = parsedInput; - - // 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', - }; - } + .action(async ({ ctx }) => { + const currentUser = (ctx as { user: User }).user; // Check if Stripe environment variables are configured const stripeSecretKey = process.env.STRIPE_SECRET_KEY; @@ -62,7 +36,7 @@ export const getActiveSubscriptionAction = actionClient try { // Find the user's most recent active subscription const subscriptions = await getSubscriptions({ - userId: session.user.id, + userId: currentUser.id, }); // console.log('get user subscriptions:', subscriptions); @@ -76,16 +50,16 @@ export const getActiveSubscriptionAction = actionClient // If found, use it if (activeSubscription) { - console.log('find active subscription for userId:', session.user.id); + console.log('find active subscription for userId:', currentUser.id); subscriptionData = activeSubscription; } else { console.log( 'no active subscription found for userId:', - session.user.id + currentUser.id ); } } else { - console.log('no subscriptions found for userId:', session.user.id); + console.log('no subscriptions found for userId:', currentUser.id); } return { diff --git a/src/actions/get-credit-balance.ts b/src/actions/get-credit-balance.ts index 1e0b6ba..da097b8 100644 --- a/src/actions/get-credit-balance.ts +++ b/src/actions/get-credit-balance.ts @@ -1,21 +1,16 @@ 'use server'; import { getUserCredits } from '@/credits/credits'; -import { getSession } from '@/lib/server'; -import { createSafeActionClient } from 'next-safe-action'; - -const actionClient = createSafeActionClient(); +import type { User } from '@/lib/auth-types'; +import { userActionClient } from '@/lib/safe-action'; /** * Get current user's credits */ -export const getCreditBalanceAction = actionClient.action(async () => { - const session = await getSession(); - if (!session) { - console.warn('unauthorized request to get credit balance'); - return { success: false, error: 'Unauthorized' }; +export const getCreditBalanceAction = userActionClient.action( + async ({ ctx }) => { + const user = (ctx as { user: User }).user; + const credits = await getUserCredits(user.id); + return { success: true, credits }; } - - const credits = await getUserCredits(session.user.id); - return { success: true, credits }; -}); +); diff --git a/src/actions/get-credit-stats.ts b/src/actions/get-credit-stats.ts index d97cc94..7e5c634 100644 --- a/src/actions/get-credit-stats.ts +++ b/src/actions/get-credit-stats.ts @@ -3,34 +3,23 @@ import { CREDIT_TRANSACTION_TYPE } from '@/credits/types'; import { getDb } from '@/db'; 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 { and, eq, gte, isNotNull, lte, sql, sum } from 'drizzle-orm'; -import { createSafeActionClient } from 'next-safe-action'; const CREDITS_EXPIRATION_DAYS = 31; const CREDITS_MONTHLY_DAYS = 31; -// Create a safe action client -const actionClient = createSafeActionClient(); - /** * Get credit statistics for a user */ -export const getCreditStatsAction = actionClient.action(async () => { +export const getCreditStatsAction = userActionClient.action(async ({ ctx }) => { try { - const session = await getSession(); - if (!session) { - console.warn('unauthorized request to get credit stats'); - return { - success: false, - error: 'Unauthorized', - }; - } + const currentUser = (ctx as { user: User }).user; + const userId = currentUser.id; const db = await getDb(); - const userId = session.user.id; - // Get credits expiring in the next CREDITS_EXPIRATION_DAYS days const expirationDaysFromNow = addDays(new Date(), CREDITS_EXPIRATION_DAYS); const expiringCredits = await db diff --git a/src/actions/get-credit-transactions.ts b/src/actions/get-credit-transactions.ts index ded99f0..ec40ae5 100644 --- a/src/actions/get-credit-transactions.ts +++ b/src/actions/get-credit-transactions.ts @@ -2,14 +2,11 @@ import { getDb } from '@/db'; 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 { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Define the schema for getCreditTransactions parameters const getCreditTransactionsSchema = z.object({ pageIndex: z.number().min(0).default(0), @@ -40,23 +37,17 @@ const sortFieldMap = { } as const; // Create a safe action for getting credit transactions -export const getCreditTransactionsAction = actionClient +export const getCreditTransactionsAction = userActionClient .schema(getCreditTransactionsSchema) - .action(async ({ parsedInput }) => { + .action(async ({ parsedInput, ctx }) => { try { - const session = await getSession(); - if (!session) { - return { - success: false, - error: 'Unauthorized', - }; - } const { pageIndex, pageSize, search, sorting } = parsedInput; + const currentUser = (ctx as { user: User }).user; // search by type, amount, paymentId, description, and restrict to current user const where = search ? and( - eq(creditTransaction.userId, session.user.id), + eq(creditTransaction.userId, currentUser.id), or( ilike(creditTransaction.type, `%${search}%`), ilike(creditTransaction.amount, `%${search}%`), @@ -65,7 +56,7 @@ export const getCreditTransactionsAction = actionClient ilike(creditTransaction.description, `%${search}%`) ) ) - : eq(creditTransaction.userId, session.user.id); + : eq(creditTransaction.userId, currentUser.id); const offset = pageIndex * pageSize; diff --git a/src/actions/get-lifetime-status.ts b/src/actions/get-lifetime-status.ts index baa6753..65635bb 100644 --- a/src/actions/get-lifetime-status.ts +++ b/src/actions/get-lifetime-status.ts @@ -2,16 +2,13 @@ import { getDb } from '@/db'; import { payment } from '@/db/schema'; +import type { User } from '@/lib/auth-types'; import { findPlanByPriceId, getAllPricePlans } from '@/lib/price-plan'; -import { getSession } from '@/lib/server'; +import { userActionClient } from '@/lib/safe-action'; import { PaymentTypes } from '@/payment/types'; import { and, eq } from 'drizzle-orm'; -import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Input schema const schema = z.object({ 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, * for example, just check the planId is `lifetime` or not. */ -export const getLifetimeStatusAction = actionClient +export const getLifetimeStatusAction = userActionClient .schema(schema) - .action(async ({ parsedInput }) => { - const { userId } = parsedInput; - - // 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', - }; - } + .action(async ({ ctx }) => { + const currentUser = (ctx as { user: User }).user; + const userId = currentUser.id; try { // Get lifetime plans diff --git a/src/actions/get-users.ts b/src/actions/get-users.ts index 45747f1..776ee80 100644 --- a/src/actions/get-users.ts +++ b/src/actions/get-users.ts @@ -3,14 +3,10 @@ import { getDb } from '@/db'; import { user } from '@/db/schema'; import { isDemoWebsite } from '@/lib/demo'; -import { getSession } from '@/lib/server'; +import { adminActionClient } from '@/lib/safe-action'; import { asc, desc, ilike, or, sql } from 'drizzle-orm'; -import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Define the schema for getUsers parameters const getUsersSchema = z.object({ pageIndex: z.number().min(0).default(0), @@ -40,17 +36,9 @@ const sortFieldMap = { } as const; // Create a safe action for getting users -export const getUsersAction = actionClient +export const getUsersAction = adminActionClient .schema(getUsersSchema) .action(async ({ parsedInput }) => { - const session = await getSession(); - if (!session || session.user.role !== 'admin') { - return { - success: false, - error: 'Unauthorized', - }; - } - try { const { pageIndex, pageSize, search, sorting } = parsedInput; diff --git a/src/actions/send-message.ts b/src/actions/send-message.ts index 2dae548..0c09aaf 100644 --- a/src/actions/send-message.ts +++ b/src/actions/send-message.ts @@ -1,14 +1,11 @@ 'use server'; import { websiteConfig } from '@/config/website'; +import { actionClient } from '@/lib/safe-action'; import { sendEmail } from '@/mail'; import { getLocale } from 'next-intl/server'; -import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - /** * DOC: When using Zod for validation, how can I localize error messages? * https://next-intl.dev/docs/environments/actions-metadata-route-handlers#server-actions diff --git a/src/actions/subscribe-newsletter.ts b/src/actions/subscribe-newsletter.ts index e3e3037..ef25e04 100644 --- a/src/actions/subscribe-newsletter.ts +++ b/src/actions/subscribe-newsletter.ts @@ -1,14 +1,11 @@ 'use server'; +import { actionClient } from '@/lib/safe-action'; import { sendEmail } from '@/mail'; import { subscribe } from '@/newsletter'; import { getLocale } from 'next-intl/server'; -import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Newsletter schema for validation const newsletterSchema = z.object({ email: z.email({ error: 'Please enter a valid email address' }), diff --git a/src/actions/unsubscribe-newsletter.ts b/src/actions/unsubscribe-newsletter.ts index 32dfe4d..b8101e3 100644 --- a/src/actions/unsubscribe-newsletter.ts +++ b/src/actions/unsubscribe-newsletter.ts @@ -1,30 +1,18 @@ 'use server'; -import { getSession } from '@/lib/server'; +import { userActionClient } from '@/lib/safe-action'; import { unsubscribe } from '@/newsletter'; -import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Newsletter schema for validation const newsletterSchema = z.object({ email: z.email({ error: 'Please enter a valid email address' }), }); // Create a safe action for newsletter unsubscription -export const unsubscribeNewsletterAction = actionClient +export const unsubscribeNewsletterAction = userActionClient .schema(newsletterSchema) .action(async ({ parsedInput: { email } }) => { - const session = await getSession(); - if (!session) { - return { - success: false, - error: 'Unauthorized', - }; - } - try { const unsubscribed = await unsubscribe(email); diff --git a/src/actions/validate-captcha.ts b/src/actions/validate-captcha.ts index 1dc85df..b8f0dcd 100644 --- a/src/actions/validate-captcha.ts +++ b/src/actions/validate-captcha.ts @@ -1,12 +1,9 @@ 'use server'; import { validateTurnstileToken } from '@/lib/captcha'; -import { createSafeActionClient } from 'next-safe-action'; +import { actionClient } from '@/lib/safe-action'; import { z } from 'zod'; -// Create a safe action client -const actionClient = createSafeActionClient(); - // Captcha validation schema const captchaSchema = z.object({ captchaToken: z.string().min(1, { error: 'Captcha token is required' }), diff --git a/src/lib/safe-action.ts b/src/lib/safe-action.ts new file mode 100644 index 0000000..12f37d1 --- /dev/null +++ b/src/lib/safe-action.ts @@ -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 }); +});