feat: add admin authentication and management features
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions

- 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:
Najjar\NajjarV02
2026-04-13 17:57:59 +04:00
parent ec0f991a30
commit 05b540997e
30 changed files with 3338 additions and 362 deletions
+68 -3
View File
@@ -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>