forked from hazem/yslootahrobotics
- 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.
173 lines
5.3 KiB
TypeScript
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',
|
|
};
|