feat: add visual admin panel with page builder and settings editor
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
This commit is contained in:
68
app/admin/components/AdminLogin.tsx
Normal file
68
app/admin/components/AdminLogin.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export default function AdminLogin({ onSuccess }: Props) {
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
onSuccess();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || 'Login failed');
|
||||
}
|
||||
} catch {
|
||||
setError('Connection error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-login">
|
||||
<form className="admin-login-card" onSubmit={handleSubmit}>
|
||||
<div className="admin-login-header">
|
||||
<h1>Immich Folio</h1>
|
||||
<p>Admin Panel</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-field">
|
||||
<label htmlFor="admin-password">Password</label>
|
||||
<input
|
||||
id="admin-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter admin password"
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="admin-error">{error}</div>}
|
||||
|
||||
<button type="submit" className="admin-btn admin-btn-primary" disabled={loading}>
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user