Merge remote-tracking branch 'origin/main' into cloudflare
This commit is contained in:
commit
35ddf5e08e
@ -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
24
scripts/list-users.ts
Normal 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();
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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',
|
||||||
|
@ -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 {
|
||||||
|
@ -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 };
|
|
||||||
});
|
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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' }),
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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' }),
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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
57
src/lib/safe-action.ts
Normal 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 });
|
||||||
|
});
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user