Najjar\NajjarV02 05b540997e 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.
2026-04-13 17:57:59 +04:00

173 lines
5.3 KiB
TypeScript

'use client';
import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation';
export default function AdminLoginPage() {
const router = useRouter();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const res = await fetch('/api/admin/login/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (res.ok) {
router.push('/admin/');
router.refresh();
} else {
setError(data.error ?? 'Login failed');
}
} catch {
setError('Network error. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div style={pageStyle}>
<div style={cardStyle}>
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<div style={{
width: '48px',
height: '48px',
borderRadius: '12px',
background: 'rgba(59, 130, 246, 0.08)',
border: '1px solid rgba(59, 130, 246, 0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 1rem',
fontSize: '1.25rem',
}}>
🔐
</div>
<h1 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#1a1a2e', margin: 0, marginBottom: '0.25rem' }}>
Admin Login
</h1>
<p style={{ fontSize: '0.8rem', color: '#94a3b8', margin: 0 }}>
Lootah Robotics G1 Configurator
</p>
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<label style={labelStyle} htmlFor="username">Username</label>
<input
id="username"
type="text"
autoComplete="username"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
style={inputStyle}
onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)')}
onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)')}
placeholder="admin"
/>
</div>
<div>
<label style={labelStyle} htmlFor="password">Password</label>
<input
id="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
style={inputStyle}
onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)')}
onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)')}
placeholder="••••••••"
/>
</div>
{error && (
<div style={{
padding: '0.6rem 0.875rem',
borderRadius: '0.375rem',
background: 'rgba(239, 68, 68, 0.06)',
border: '1px solid rgba(239, 68, 68, 0.2)',
color: '#dc2626',
fontSize: '0.8rem',
}}>
{error}
</div>
)}
<button
type="submit"
disabled={loading}
style={{
padding: '0.7rem',
borderRadius: '0.5rem',
border: '1px solid rgba(59, 130, 246, 0.3)',
background: loading ? 'rgba(148, 163, 184, 0.1)' : 'rgba(59, 130, 246, 0.08)',
color: loading ? '#94a3b8' : '#2563eb',
fontSize: '0.875rem',
fontWeight: 600,
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
marginTop: '0.25rem',
}}
>
{loading ? 'Signing in…' : 'Sign In'}
</button>
</form>
</div>
</div>
);
}
const pageStyle: React.CSSProperties = {
minHeight: '100vh',
background: '#ffffff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
fontFamily: 'system-ui, -apple-system, sans-serif',
};
const cardStyle: React.CSSProperties = {
width: '100%',
maxWidth: '380px',
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(0, 0, 0, 0.06)',
borderRadius: '1rem',
padding: '2rem',
};
const labelStyle: React.CSSProperties = {
display: 'block',
fontSize: '0.75rem',
fontWeight: 600,
color: '#374151',
marginBottom: '0.375rem',
letterSpacing: '0.02em',
};
const inputStyle: React.CSSProperties = {
width: '100%',
padding: '0.6rem 0.875rem',
borderRadius: '0.5rem',
border: '1px solid rgba(0, 0, 0, 0.1)',
background: '#ffffff',
color: '#1a1a2e',
fontSize: '0.875rem',
outline: 'none',
transition: 'border-color 0.2s ease',
boxSizing: 'border-box',
};