feat: add admin authentication and management features
- Implemented Prisma schema with models for AdminUser, AppSettings, and Snapshot. - Created seed script to initialize the database with an admin user and JWT secret. - Developed admin login page with form handling and error management. - Added API routes for admin login, logout, change password, and JWT verification. - Integrated Stripe for payment intent management in admin orders. - Established middleware for protecting admin routes with JWT authentication. - Created Zustand stores for managing persona and snapshot states.
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { useOrderStore, orderStore, type CheckoutStep } from '@/store/useOrderStore';
|
||||
import { ShippingStep } from './checkout/ShippingStep';
|
||||
import { PaymentStep } from './checkout/PaymentStep';
|
||||
import { ReviewStep } from './checkout/ReviewStep';
|
||||
import { ConfirmationStep } from './checkout/ConfirmationStep';
|
||||
|
||||
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
|
||||
|
||||
const STEPS: { id: CheckoutStep; label: string }[] = [
|
||||
{ id: 'shipping', label: 'Shipping' },
|
||||
{ id: 'payment', label: 'Payment' },
|
||||
@@ -19,6 +23,10 @@ function getStepIndex(step: CheckoutStep): number {
|
||||
|
||||
export function CheckoutOverlay() {
|
||||
const step = useOrderStore((s) => s.step);
|
||||
const clientSecret = useOrderStore((s) => s.payment.clientSecret);
|
||||
const paymentStatus = useOrderStore((s) => s.payment.status);
|
||||
const paymentError = useOrderStore((s) => s.payment.errorMessage);
|
||||
const [isCreatingIntent, setIsCreatingIntent] = useState(false);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
orderStore.getState().resetOrder();
|
||||
@@ -33,6 +41,16 @@ export function CheckoutOverlay() {
|
||||
}
|
||||
}, [step]);
|
||||
|
||||
// Create payment intent when entering the payment step (only once)
|
||||
useEffect(() => {
|
||||
if (step === 'payment' && !clientSecret && !isCreatingIntent && paymentStatus !== 'failed') {
|
||||
setIsCreatingIntent(true);
|
||||
orderStore.getState().createPaymentIntent().finally(() => {
|
||||
setIsCreatingIntent(false);
|
||||
});
|
||||
}
|
||||
}, [step, clientSecret, isCreatingIntent, paymentStatus]);
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
if (step === 'config') return;
|
||||
@@ -153,8 +171,55 @@ export function CheckoutOverlay() {
|
||||
{/* Step Content */}
|
||||
<div style={{ padding: '1.5rem' }}>
|
||||
{step === 'shipping' && <ShippingStep />}
|
||||
{step === 'payment' && <PaymentStep />}
|
||||
{step === 'review' && <ReviewStep />}
|
||||
{(step === 'payment' || step === 'review') && clientSecret ? (
|
||||
<Elements
|
||||
stripe={stripePromise}
|
||||
options={{
|
||||
clientSecret,
|
||||
appearance: {
|
||||
theme: 'stripe',
|
||||
variables: {
|
||||
colorPrimary: '#2563eb',
|
||||
borderRadius: '0.375rem',
|
||||
fontFamily: 'inherit',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Keep PaymentElement mounted (hidden) during review so confirmPayment works */}
|
||||
<div style={{ display: step === 'payment' ? 'block' : 'none' }}>
|
||||
<PaymentStep />
|
||||
</div>
|
||||
{step === 'review' && <ReviewStep />}
|
||||
</Elements>
|
||||
) : (step === 'payment' || step === 'review') && !clientSecret ? (
|
||||
<div style={{ textAlign: 'center', padding: '2rem', fontSize: '0.85rem' }}>
|
||||
{paymentStatus === 'failed' ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', alignItems: 'center' }}>
|
||||
<div style={{ color: '#dc2626' }}>{paymentError || 'Failed to initialize payment'}</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
orderStore.getState().setPayment({ status: 'idle', errorMessage: '' });
|
||||
}}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
borderRadius: '0.375rem',
|
||||
border: '1px solid rgba(59, 130, 246, 0.5)',
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
color: '#2563eb',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: '#94a3b8' }}>Initializing secure payment...</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{step === 'confirmed' && <ConfirmationStep />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user