api: add subscription API (#1491)
This commit is contained in:
@@ -7,3 +7,6 @@ NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX2ViNXowbVRxWm8yZm5YYnZGNmE3bFh5TThp
|
||||
|
||||
NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64="aHR0cHM6Ly9yZWFkZXN0LnN1cGFiYXNlLmNv"
|
||||
NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64="ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw=="
|
||||
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_DEV_BASE64="cGtfdGVzdF81MVJmQmdLRTdSWW5pTWsxc0tDV2RUd2hMZzcySzk4eDRWcjlIdDdsRFBONngzcnpZYmhydGtNQnpDdzZKbHFaRVVITVp5eVNjVXhCZXVkcGppWTk0WXNHcDAweFlRRnRRaUU="
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_BASE64="cGtfbGl2ZV81MVFYN3dRRU5ndjJFOUxQRHpZUlE5TlJJeTNjd09EZ1AzSkNFRHRPWlFtdFJWc3Brd053ZE1NNUpIVnVPTmJWcjZ3VGFCMUNZR1pJMmRPVWppTkY0bHJvVjAwalE4TkpkdWk="
|
||||
|
||||
@@ -8,11 +8,14 @@ if (isDev) {
|
||||
initOpenNextCloudflareForDev();
|
||||
}
|
||||
|
||||
const exportOutput = appPlatform !== 'web' && !isDev;
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Ensure Next.js uses SSG instead of SSR
|
||||
// https://nextjs.org/docs/pages/building-your-application/deploying/static-exports
|
||||
output: appPlatform === 'web' || isDev ? undefined : 'export',
|
||||
output: exportOutput ? 'export' : undefined,
|
||||
pageExtensions: exportOutput ? ['jsx', 'tsx'] : ['js', 'jsx', 'ts', 'tsx'],
|
||||
// Note: This feature is required to use the Next.js Image component in SSG mode.
|
||||
// See https://nextjs.org/docs/messages/export-image-api for different workarounds.
|
||||
images: {
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"@ducanh2912/next-pwa": "^10.2.9",
|
||||
"@fabianlars/tauri-plugin-oauth": "2",
|
||||
"@opennextjs/cloudflare": "^1.3.1",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2.49.10",
|
||||
@@ -83,6 +84,7 @@
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-window": "^1.8.11",
|
||||
"semver": "^7.7.1",
|
||||
"stripe": "^18.2.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import Stripe from 'stripe';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getStripe } from '@/libs/stripe/server';
|
||||
import { createSubscription } from '@/utils/stripe';
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { sessionId } = await request.json();
|
||||
|
||||
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
|
||||
if (!user || !token) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stripe = getStripe();
|
||||
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
||||
|
||||
if (session.payment_status === 'paid') {
|
||||
const customerId = session.customer as string;
|
||||
const subscriptionId = session.subscription as string;
|
||||
await createSubscription(user.id, customerId, subscriptionId);
|
||||
}
|
||||
|
||||
return NextResponse.json({ session });
|
||||
} catch (error) {
|
||||
if (error instanceof Stripe.errors.StripeError) {
|
||||
console.error('Stripe error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getStripe } from '@/libs/stripe/server';
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const { priceId, metadata = {} } = await request.json();
|
||||
|
||||
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
|
||||
if (!user || !token) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
|
||||
}
|
||||
|
||||
const enhancedMetadata = {
|
||||
...metadata,
|
||||
userId: user.id,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: customerData } = await supabase
|
||||
.from('customers')
|
||||
.select('stripe_customer_id')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
let customerId;
|
||||
if (!customerData?.stripe_customer_id) {
|
||||
const stripe = getStripe();
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
customerId = customer.id;
|
||||
await supabase.from('customers').insert({
|
||||
user_id: user.id,
|
||||
stripe_customer_id: customerId,
|
||||
});
|
||||
} else {
|
||||
customerId = customerData.stripe_customer_id;
|
||||
}
|
||||
|
||||
const stripe = getStripe();
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
metadata: enhancedMetadata,
|
||||
success_url: `${request.headers.get('origin')}/user/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${request.headers.get('origin')}/user`,
|
||||
});
|
||||
|
||||
return NextResponse.json({ sessionId: session.id, url: session.url });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: 'Error creating checkout session' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Stripe from 'stripe';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getStripe } from '@/libs/stripe/server';
|
||||
import { UserPlan } from '@/types/user';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const stripe = getStripe();
|
||||
const prices = await stripe.prices.list({
|
||||
expand: ['data.product'],
|
||||
active: true,
|
||||
type: 'recurring',
|
||||
});
|
||||
|
||||
const plans = prices.data
|
||||
.filter((price) => {
|
||||
const product = price.product as Stripe.Product;
|
||||
return product.active === true;
|
||||
})
|
||||
.map((price) => {
|
||||
const product = price.product as Stripe.Product & {
|
||||
metadata: { plan: UserPlan };
|
||||
};
|
||||
return {
|
||||
plan: product.metadata.plan,
|
||||
price_id: price.id,
|
||||
price: price.unit_amount,
|
||||
currency: price.currency,
|
||||
interval: price.recurring?.interval,
|
||||
product: price.product,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json(plans);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: 'Error fetching subscription plans' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getStripe } from '@/libs/stripe/server';
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
|
||||
if (!user || !token) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: customerData } = await supabase
|
||||
.from('customers')
|
||||
.select('stripe_customer_id')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
if (!customerData?.stripe_customer_id) {
|
||||
throw new Error('Customer not found');
|
||||
}
|
||||
|
||||
const stripe = getStripe();
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: customerData.stripe_customer_id,
|
||||
return_url: `${request.headers.get('origin')}/user`,
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: session.url });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: 'Error creating portal session' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import Stripe from 'stripe';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getStripe } from '@/libs/stripe/server';
|
||||
import { createSubscription } from '@/utils/stripe';
|
||||
import { createSupabaseAdminClient } from '@/utils/supabase';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.text();
|
||||
const signature = request.headers.get('stripe-signature');
|
||||
|
||||
if (!signature) {
|
||||
return NextResponse.json({ error: 'Missing Stripe signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
const stripe = getStripe();
|
||||
|
||||
let event;
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
body,
|
||||
signature,
|
||||
process.env['STRIPE_WEBHOOK_SECRET']!,
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`Webhook signature verification failed: ${message}`);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Webhook signature verification failed: ${message}`,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed':
|
||||
const session = event.data.object;
|
||||
const userId = session.metadata?.['userId'];
|
||||
if (userId) {
|
||||
if (session.mode === 'subscription') {
|
||||
await handleSuccessfulSubscription(session, userId);
|
||||
} else {
|
||||
await handleSuccessfulPayment(session, userId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'invoice.paid':
|
||||
await handleSuccessfulInvoice(event.data.object);
|
||||
break;
|
||||
|
||||
case 'invoice.payment_failed':
|
||||
await handleFailedInvoice(event.data.object);
|
||||
break;
|
||||
|
||||
case 'customer.subscription.deleted':
|
||||
await handleSubscriptionCancelled(event.data.object);
|
||||
break;
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Webhook error:', message);
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSuccessfulPayment(session: Stripe.Checkout.Session, userId: string) {
|
||||
const supabase = createSupabaseAdminClient();
|
||||
await supabase.from('payments').insert({
|
||||
user_id: userId,
|
||||
stripe_checkout_id: session.id,
|
||||
amount: session.amount_total,
|
||||
currency: session.currency,
|
||||
status: 'completed',
|
||||
payment_intent: session.payment_intent,
|
||||
payment_method: session.payment_method_types?.[0] || 'unknown',
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSuccessfulSubscription(session: Stripe.Checkout.Session, userId: string) {
|
||||
const customerId = session.customer as string;
|
||||
const subscriptionId = session.subscription as string;
|
||||
|
||||
await createSubscription(userId, customerId, subscriptionId);
|
||||
}
|
||||
|
||||
async function handleSuccessfulInvoice(invoice: Stripe.Invoice) {
|
||||
const customerId = invoice.customer;
|
||||
const subscriptionId = invoice.parent?.subscription_details?.subscription;
|
||||
|
||||
if (!subscriptionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const supabase = createSupabaseAdminClient();
|
||||
const { data: customerData } = await supabase
|
||||
.from('customers')
|
||||
.select('user_id')
|
||||
.eq('stripe_customer_id', customerId)
|
||||
.single();
|
||||
|
||||
if (!customerData?.user_id) {
|
||||
console.error('Customer not found:', customerId);
|
||||
return;
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('subscriptions')
|
||||
.update({
|
||||
status: 'active',
|
||||
current_period_end: new Date(invoice.lines.data[0]!.period.end * 1000).toISOString(),
|
||||
})
|
||||
.eq('stripe_subscription_id', subscriptionId);
|
||||
|
||||
await supabase
|
||||
.from('plans')
|
||||
.update({
|
||||
status: 'active',
|
||||
})
|
||||
.eq('user_id', customerData.user_id);
|
||||
}
|
||||
|
||||
async function handleFailedInvoice(invoice: Stripe.Invoice) {
|
||||
const customerId = invoice.customer;
|
||||
const subscriptionId = invoice.parent?.subscription_details?.subscription;
|
||||
|
||||
if (!subscriptionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const supabase = createSupabaseAdminClient();
|
||||
const { data: customerData } = await supabase
|
||||
.from('customers')
|
||||
.select('user_id')
|
||||
.eq('stripe_customer_id', customerId)
|
||||
.single();
|
||||
|
||||
if (!customerData?.user_id) {
|
||||
console.error('Customer not found:', customerId);
|
||||
return;
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('subscriptions')
|
||||
.update({
|
||||
status: 'past_due',
|
||||
})
|
||||
.eq('stripe_subscription_id', subscriptionId);
|
||||
|
||||
await supabase
|
||||
.from('plans')
|
||||
.update({
|
||||
status: 'past_due',
|
||||
})
|
||||
.eq('id', customerData.user_id);
|
||||
}
|
||||
|
||||
async function handleSubscriptionCancelled(subscription: Stripe.Subscription) {
|
||||
const subscriptionId = subscription.id;
|
||||
|
||||
const supabase = createSupabaseAdminClient();
|
||||
await supabase
|
||||
.from('subscriptions')
|
||||
.update({
|
||||
status: 'cancelled',
|
||||
cancelled_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('stripe_subscription_id', subscriptionId);
|
||||
|
||||
const { data: subscriptionData } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('user_id')
|
||||
.eq('stripe_subscription_id', subscriptionId)
|
||||
.single();
|
||||
|
||||
if (subscriptionData?.user_id) {
|
||||
await supabase
|
||||
.from('plans')
|
||||
.update({
|
||||
plan: 'free',
|
||||
status: 'cancelled',
|
||||
})
|
||||
.eq('id', subscriptionData.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
// This is needed to parse the body as a stream for the webhook signature verification
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||
|
||||
let stripePromise: Promise<Stripe | null>;
|
||||
|
||||
export const getStripe = () => {
|
||||
if (!stripePromise) {
|
||||
const publishableKey =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? process.env['NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_BASE64']
|
||||
: process.env['NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_DEV_BASE64'];
|
||||
stripePromise = loadStripe(atob(publishableKey!));
|
||||
}
|
||||
return stripePromise;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
let stripe: Stripe | null;
|
||||
|
||||
export const getStripe = () => {
|
||||
if (!stripe) {
|
||||
const stripeSecretKey =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? process.env['STRIPE_SECRET_KEY']
|
||||
: process.env['STRIPE_SECRET_KEY_DEV'];
|
||||
stripe = new Stripe(stripeSecretKey!, {
|
||||
httpClient: Stripe.createFetchHttpClient(),
|
||||
});
|
||||
}
|
||||
return stripe;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const allowedOrigins = [
|
||||
'https://web.readest.com',
|
||||
'https://tauri.localhost',
|
||||
'http://tauri.localhost',
|
||||
'tauri://localhost',
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3001',
|
||||
];
|
||||
|
||||
const corsOptions = {
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
};
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const origin = request.headers.get('origin') ?? '';
|
||||
const isAllowedOrigin = allowedOrigins.includes(origin);
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
const preflightHeaders = new Headers({
|
||||
...corsOptions,
|
||||
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
|
||||
});
|
||||
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: preflightHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
const response = NextResponse.next();
|
||||
|
||||
if (isAllowedOrigin) {
|
||||
response.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
|
||||
Object.entries(corsOptions).forEach(([key, value]) => {
|
||||
response.headers.set(key, value);
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/api/stripe/:path*'],
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import crypto from 'crypto';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import { getDailyTranslationPlanData, getUserPlan } from '@/utils/access';
|
||||
import { getDailyTranslationPlanData, getUserPlan, validateUserAndToken } from '@/utils/access';
|
||||
import { ErrorCodes } from '@/services/translators';
|
||||
|
||||
const DEFAULT_DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate';
|
||||
@@ -19,19 +18,6 @@ interface CloudflareEnv {
|
||||
TRANSLATIONS_KV?: KVNamespace;
|
||||
}
|
||||
|
||||
const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
if (!authHeader) return {};
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser(token);
|
||||
|
||||
if (error || !user) return {};
|
||||
return { user, token };
|
||||
};
|
||||
|
||||
const LANG_V2_V1_MAP: Record<string, string> = {
|
||||
'ZH-HANS': 'ZH',
|
||||
'ZH-HANT': 'ZH-TW',
|
||||
@@ -58,7 +44,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const env = (getCloudflareContext().env || {}) as CloudflareEnv;
|
||||
const hasKVCache = !!env['TRANSLATIONS_KV'];
|
||||
|
||||
const { user, token } = await getUserAndToken(req.headers['authorization']);
|
||||
const { user, token } = await validateUserAndToken(req.headers['authorization']);
|
||||
const { DEEPL_PRO_API, DEEPL_FREE_API } = process.env;
|
||||
const deepFreeApiUrl = DEEPL_FREE_API || DEFAULT_DEEPL_FREE_API;
|
||||
const deeplProApiUrl = DEEPL_PRO_API || DEFAULT_DEEPL_PRO_API;
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { supabase, createSupabaseClient } from '@/utils/supabase';
|
||||
import { createSupabaseClient } from '@/utils/supabase';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { getDownloadSignedUrl } from '@/utils/object';
|
||||
|
||||
const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
if (!authHeader) return {};
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser(token);
|
||||
|
||||
if (error || !user) return {};
|
||||
return { user, token };
|
||||
};
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await runMiddleware(req, res, corsAllMethods);
|
||||
@@ -24,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
try {
|
||||
const { user, token } = await getUserAndToken(req.headers['authorization']);
|
||||
const { user, token } = await validateUserAndToken(req.headers['authorization']);
|
||||
if (!user || !token) {
|
||||
return res.status(403).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { supabase, createSupabaseClient } from '@/utils/supabase';
|
||||
import { createSupabaseClient } from '@/utils/supabase';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { getStoragePlanData } from '@/utils/access';
|
||||
import { getStoragePlanData, validateUserAndToken } from '@/utils/access';
|
||||
import { getUploadSignedUrl } from '@/utils/object';
|
||||
|
||||
const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
if (!authHeader) return {};
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser(token);
|
||||
|
||||
if (error || !user) return {};
|
||||
return { user, token };
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await runMiddleware(req, res, corsAllMethods);
|
||||
|
||||
@@ -25,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
try {
|
||||
const { user, token } = await getUserAndToken(req.headers['authorization']);
|
||||
const { user, token } = await validateUserAndToken(req.headers['authorization']);
|
||||
if (!user || !token) {
|
||||
return res.status(403).json({ error: 'Not authenticated' });
|
||||
}
|
||||
@@ -74,11 +61,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadUrl = await getUploadSignedUrl(
|
||||
fileKey,
|
||||
objSize,
|
||||
1800,
|
||||
);
|
||||
const uploadUrl = await getUploadSignedUrl(fileKey, objSize, 1800);
|
||||
|
||||
res.status(200).json({
|
||||
uploadUrl,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PostgrestError } from '@supabase/supabase-js';
|
||||
import { supabase, createSupabaseClient } from '@/utils/supabase';
|
||||
import { createSupabaseClient } from '@/utils/supabase';
|
||||
import { BookDataRecord } from '@/types/book';
|
||||
import { transformBookConfigToDB } from '@/utils/transform';
|
||||
import { transformBookNoteToDB } from '@/utils/transform';
|
||||
import { transformBookToDB } from '@/utils/transform';
|
||||
import { runMiddleware, corsAllMethods } from '@/utils/cors';
|
||||
import { SyncData, SyncResult, SyncType } from '@/libs/sync';
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
|
||||
const transformsToDB = {
|
||||
books: transformBookToDB,
|
||||
@@ -25,31 +26,10 @@ type TableName = keyof typeof transformsToDB;
|
||||
|
||||
type DBError = { table: TableName; error: PostgrestError };
|
||||
|
||||
const getUserAndToken = async (req: NextRequest) => {
|
||||
const authHeader = req.headers.get('authorization');
|
||||
if (!authHeader) return {};
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser(token);
|
||||
if (error?.message === 'fetch failed') {
|
||||
return { error: 'Network error' };
|
||||
} else if (error || !user) {
|
||||
return { error: 'Not authenticated' };
|
||||
}
|
||||
return { user, token };
|
||||
} catch {
|
||||
return { error: 'Network error' };
|
||||
}
|
||||
};
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { user, token, error } = await getUserAndToken(req);
|
||||
if (!user || !token || error) {
|
||||
return NextResponse.json({ error: error || 'Unknown error' }, { status: 401 });
|
||||
const { user, token } = await validateUserAndToken(req.headers.get('authorization'));
|
||||
if (!user || !token) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
|
||||
}
|
||||
const supabase = createSupabaseClient(token);
|
||||
|
||||
@@ -121,9 +101,9 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { user, token, error } = await getUserAndToken(req);
|
||||
if (!user || !token || error) {
|
||||
return NextResponse.json({ error: error || 'Unknown error' }, { status: 401 });
|
||||
const { user, token } = await validateUserAndToken(req.headers.get('authorization'));
|
||||
if (!user || !token) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
|
||||
}
|
||||
const supabase = createSupabaseClient(token);
|
||||
|
||||
|
||||
@@ -555,9 +555,9 @@ export const DEFAULT_STORAGE_QUOTA: UserStorageQuota = {
|
||||
};
|
||||
|
||||
export const DEFAULT_DAILY_TRANSLATION_QUOTA: UserDailyTranslationQuota = {
|
||||
free: 50 * 1024,
|
||||
plus: 500 * 1024,
|
||||
pro: 1024 * 1024,
|
||||
free: 10 * 1024,
|
||||
plus: 50 * 1024,
|
||||
pro: 200 * 1024,
|
||||
};
|
||||
|
||||
export const DOUBLE_CLICK_INTERVAL_THRESHOLD_MS = 250;
|
||||
|
||||
@@ -76,7 +76,7 @@ export const getUserID = async (): Promise<string | null> => {
|
||||
return data?.session?.user?.id ?? null;
|
||||
};
|
||||
|
||||
export const validateUserAndToken = async (authHeader: string | undefined) => {
|
||||
export const validateUserAndToken = async (authHeader: string | null | undefined) => {
|
||||
if (!authHeader) return {};
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import Stripe from 'stripe';
|
||||
import { getStripe } from '@/libs/stripe/server';
|
||||
import { createSupabaseAdminClient } from './supabase';
|
||||
import { UserPlan } from '@/types/user';
|
||||
|
||||
export const createSubscription = async (
|
||||
userId: string,
|
||||
customerId: string,
|
||||
subscriptionId: string,
|
||||
) => {
|
||||
const stripe = getStripe();
|
||||
const supabase = createSupabaseAdminClient();
|
||||
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
|
||||
expand: ['items.data.price.product'],
|
||||
});
|
||||
const subscriptionItem = subscription.items.data[0]!;
|
||||
const priceId = subscriptionItem.price.id;
|
||||
const product = subscriptionItem.price.product as Stripe.Product & {
|
||||
metadata: { plan: UserPlan };
|
||||
};
|
||||
const plan = product.metadata['plan'] || 'free';
|
||||
|
||||
try {
|
||||
const { data: existingSubscription } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('*')
|
||||
.eq('stripe_subscription_id', subscriptionId)
|
||||
.single();
|
||||
|
||||
const period_start = new Date(subscriptionItem.current_period_start * 1000).toISOString();
|
||||
const period_end = new Date(subscriptionItem.current_period_end * 1000).toISOString();
|
||||
if (existingSubscription) {
|
||||
await supabase
|
||||
.from('subscriptions')
|
||||
.update({
|
||||
status: subscription.status,
|
||||
current_period_start: period_start,
|
||||
current_period_end: period_end,
|
||||
})
|
||||
.eq('id', existingSubscription.id);
|
||||
} else {
|
||||
await supabase.from('subscriptions').insert({
|
||||
user_id: userId,
|
||||
stripe_customer_id: customerId,
|
||||
stripe_subscription_id: subscriptionId,
|
||||
stripe_price_id: priceId,
|
||||
status: subscription.status,
|
||||
current_period_start: period_start,
|
||||
current_period_end: period_end,
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking existing subscription:', error);
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('plans')
|
||||
.update({
|
||||
plan: plan,
|
||||
status: subscription.status,
|
||||
})
|
||||
.eq('id', userId);
|
||||
};
|
||||
Generated
+81
-13
@@ -53,6 +53,9 @@ importers:
|
||||
'@opennextjs/cloudflare':
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(wrangler@4.21.0)
|
||||
'@stripe/stripe-js':
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0
|
||||
'@supabase/auth-ui-react':
|
||||
specifier: ^0.4.7
|
||||
version: 0.4.7(@supabase/supabase-js@2.49.10)
|
||||
@@ -173,6 +176,9 @@ importers:
|
||||
semver:
|
||||
specifier: ^7.7.1
|
||||
version: 7.7.2
|
||||
stripe:
|
||||
specifier: ^18.2.1
|
||||
version: 18.2.1(@types/node@22.15.31)
|
||||
tinycolor2:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
@@ -329,24 +335,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/napi-linux-arm64-musl@0.35.0':
|
||||
resolution: {integrity: sha512-1EcvHPwyWpCL/96LuItBYGfeI5FaMTRvL+dHbO/hL5q1npqbb5qn+ppJwtNOjTPz8tayvgggxVk9T4C2O7taYA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@ast-grep/napi-linux-x64-gnu@0.35.0':
|
||||
resolution: {integrity: sha512-FDzNdlqmQnsiWXhnLxusw5AOfEcEM+5xtmrnAf3SBRFr86JyWD9qsynnFYC2pnP9hlMfifNH2TTmMpyGJW49Xw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/napi-linux-x64-musl@0.35.0':
|
||||
resolution: {integrity: sha512-wlmndjfBafT8u5p4DBnoRQyoCSGNuVSz7rT3TqhvlHcPzUouRWMn95epU9B1LNLyjXvr9xHeRjSktyCN28w57Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@ast-grep/napi-win32-arm64-msvc@0.35.0':
|
||||
resolution: {integrity: sha512-gkhJeYc4rrZLX2icLxalPikTLMR57DuIYLwLr9g+StHYXIsGHrbfrE6Nnbdd8Izfs34ArFCrcwdaMrGlvOPSeg==}
|
||||
@@ -1551,138 +1561,163 @@ packages:
|
||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.1.0':
|
||||
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
||||
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.1.0':
|
||||
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
|
||||
resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.2':
|
||||
resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.33.5':
|
||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.2':
|
||||
resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.33.5':
|
||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.2':
|
||||
resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.33.5':
|
||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.2':
|
||||
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.2':
|
||||
resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.2':
|
||||
resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.33.5':
|
||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||
@@ -1781,30 +1816,35 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.70':
|
||||
resolution: {integrity: sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70':
|
||||
resolution: {integrity: sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.70':
|
||||
resolution: {integrity: sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.70':
|
||||
resolution: {integrity: sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.70':
|
||||
resolution: {integrity: sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==}
|
||||
@@ -1839,24 +1879,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.3':
|
||||
resolution: {integrity: sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.3':
|
||||
resolution: {integrity: sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.3':
|
||||
resolution: {integrity: sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.3':
|
||||
resolution: {integrity: sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==}
|
||||
@@ -2007,46 +2051,55 @@ packages:
|
||||
resolution: {integrity: sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.28.0':
|
||||
resolution: {integrity: sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.28.0':
|
||||
resolution: {integrity: sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.28.0':
|
||||
resolution: {integrity: sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.28.0':
|
||||
resolution: {integrity: sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.28.0':
|
||||
resolution: {integrity: sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.28.0':
|
||||
resolution: {integrity: sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.28.0':
|
||||
resolution: {integrity: sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.28.0':
|
||||
resolution: {integrity: sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.28.0':
|
||||
resolution: {integrity: sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==}
|
||||
@@ -2492,6 +2545,10 @@ packages:
|
||||
'@stitches/core@1.2.8':
|
||||
resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==}
|
||||
|
||||
'@stripe/stripe-js@7.4.0':
|
||||
resolution: {integrity: sha512-lQHQPfXPTBeh0XFjq6PqSBAyR7umwcJbvJhXV77uGCUDD6ymXJU/f2164ydLMLCCceNuPlbV9b+1smx98efwWQ==}
|
||||
engines: {node: '>=12.16'}
|
||||
|
||||
'@supabase/auth-js@2.69.1':
|
||||
resolution: {integrity: sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==}
|
||||
|
||||
@@ -2564,30 +2621,35 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.6.0':
|
||||
resolution: {integrity: sha512-lAEzj40VGrn/dPtly9E2dfng82t1xuEdoq0fwjEk6J4oeoKJwD/Kss7vysQHSHq36nKGFoO7gpKa5mxisBGFog==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.6.0':
|
||||
resolution: {integrity: sha512-IWaMX0tQQHFzOudJ0OyC3wamFUr/Qje3qwPSu2okSiHmP1+5enSN3WZZJV5vZ979n/Mak2TFQ8+ivPeSW2fn6g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.6.0':
|
||||
resolution: {integrity: sha512-w/zlc2H3c9D4GEjAQOOh/DT701SRT9L8IvuhEMwdWWG6BQEH/+u5tgYR3sVJNDv9WXGNxTCiFV0bd/6pgreC1Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.6.0':
|
||||
resolution: {integrity: sha512-XSeuj7AWuP6Abg532wvzcgIXLlkZLhFXNDRISp7Zeb/ZNHrYTd4BcO88IPb5LVw5vVmvV5r6iTxDd9oVw/FrmA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.6.0':
|
||||
resolution: {integrity: sha512-gvmpQkj2vsU3fvJNgSzrX2mw/0DRnkfrg+1GH3wHsCoZq0/xVXG1mXZAxDg4r6e4yCeVuM6Xv2OHpF+Az9+cXA==}
|
||||
@@ -5312,10 +5374,6 @@ packages:
|
||||
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel@1.0.6:
|
||||
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel@1.1.0:
|
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5448,6 +5506,15 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
stripe@18.2.1:
|
||||
resolution: {integrity: sha512-GwB1B7WSwEBzW4dilgyJruUYhbGMscrwuyHsPUmSRKrGHZ5poSh2oU9XKdii5BFVJzXHn35geRvGJ6R8bYcp8w==}
|
||||
engines: {node: '>=12.*'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=12.x.x'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
strnum@1.0.5:
|
||||
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
|
||||
|
||||
@@ -9314,6 +9381,8 @@ snapshots:
|
||||
|
||||
'@stitches/core@1.2.8': {}
|
||||
|
||||
'@stripe/stripe-js@7.4.0': {}
|
||||
|
||||
'@supabase/auth-js@2.69.1':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
@@ -11205,7 +11274,7 @@ snapshots:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.0.6
|
||||
side-channel: 1.1.0
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
@@ -12359,13 +12428,6 @@ snapshots:
|
||||
object-inspect: 1.13.3
|
||||
side-channel-map: 1.0.1
|
||||
|
||||
side-channel@1.0.6:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.2.4
|
||||
object-inspect: 1.13.3
|
||||
|
||||
side-channel@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -12459,7 +12521,7 @@ snapshots:
|
||||
internal-slot: 1.0.7
|
||||
regexp.prototype.flags: 1.5.3
|
||||
set-function-name: 2.0.2
|
||||
side-channel: 1.0.6
|
||||
side-channel: 1.1.0
|
||||
|
||||
string.prototype.repeat@1.0.0:
|
||||
dependencies:
|
||||
@@ -12515,6 +12577,12 @@ snapshots:
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
stripe@18.2.1(@types/node@22.15.31):
|
||||
dependencies:
|
||||
qs: 6.14.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.31
|
||||
|
||||
strnum@1.0.5: {}
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.26.7)(react@19.0.0):
|
||||
|
||||
Reference in New Issue
Block a user