- Add User, Session, AuthSettings models with bcrypt password hashing - Add auth endpoints: login, logout, change-password, me - Add API key CRUD: create (orch_xxx format), list, revoke - Add admin user management: list, create, update, reset-password - Create default admin user on startup (admin/admin) - Add frontend: Login page, API Keys page, Admin Users page - Add AuthContext for session state management - Add user menu to Layout header with login/logout/settings - Add 15 integration tests for auth system - Add migration 006_auth_tables.sql
143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import './LoginPage.css';
|
|
|
|
function LoginPage() {
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const { user, login, loading: authLoading } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
// Get the return URL from location state, default to home
|
|
const from = (location.state as { from?: string })?.from || '/';
|
|
|
|
// Redirect if already logged in
|
|
useEffect(() => {
|
|
if (user && !authLoading) {
|
|
navigate(from, { replace: true });
|
|
}
|
|
}, [user, authLoading, navigate, from]);
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
|
|
if (!username.trim() || !password) {
|
|
setError('Please enter both username and password');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
await login(username, password);
|
|
navigate(from, { replace: true });
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Login failed. Please try again.');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
// Show loading while checking auth state
|
|
if (authLoading) {
|
|
return (
|
|
<div className="login-page">
|
|
<div className="login-container">
|
|
<div className="login-loading">Checking session...</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="login-page">
|
|
<div className="login-container">
|
|
<div className="login-card">
|
|
<div className="login-header">
|
|
<div className="login-logo">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M6 14 Q6 8 3 8 Q6 4 6 4 Q6 4 9 8 Q6 8 6 14" fill="currentColor" opacity="0.6"/>
|
|
<rect x="5.25" y="13" width="1.5" height="4" fill="currentColor" opacity="0.6"/>
|
|
<path d="M12 12 Q12 5 8 5 Q12 1 12 1 Q12 1 16 5 Q12 5 12 12" fill="currentColor"/>
|
|
<rect x="11.25" y="11" width="1.5" height="5" fill="currentColor"/>
|
|
<path d="M18 14 Q18 8 15 8 Q18 4 18 4 Q18 4 21 8 Q18 8 18 14" fill="currentColor" opacity="0.6"/>
|
|
<rect x="17.25" y="13" width="1.5" height="4" fill="currentColor" opacity="0.6"/>
|
|
<ellipse cx="12" cy="19" rx="9" ry="1.5" fill="currentColor" opacity="0.3"/>
|
|
</svg>
|
|
</div>
|
|
<h1>Sign in to Orchard</h1>
|
|
<p className="login-subtitle">Content-Addressable Storage</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="login-error">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
</svg>
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="login-form">
|
|
<div className="login-form-group">
|
|
<label htmlFor="username">Username</label>
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder="Enter your username"
|
|
autoComplete="username"
|
|
autoFocus
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div className="login-form-group">
|
|
<label htmlFor="password">Password</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Enter your password"
|
|
autoComplete="current-password"
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className="login-submit"
|
|
disabled={isSubmitting}
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<span className="login-spinner"></span>
|
|
Signing in...
|
|
</>
|
|
) : (
|
|
'Sign in'
|
|
)}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div className="login-footer">
|
|
<p>Artifact storage and management system</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default LoginPage;
|