Feature: Add password change functionality for admin and customers

- Add customer password change page at /account/change-password
- Add admin password change page at /admin/change-password
- Create AdminUser database table for storing admin credentials
- Admin login now checks database first, falls back to env vars
- Update admin auth to support bcrypt password hashing
- Add change password links to customer account and admin nav
- Fix customer login logs back link (was pointing to non-existent /admin)
- Add success/error message handling for password changes

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-19 09:51:52 +01:00
co-authored by Claude Haiku 4.5
parent 36cc6dddff
commit 9623ac2fdb
14 changed files with 430 additions and 29 deletions
+3
View File
@@ -36,6 +36,9 @@ export default function AdminNav() {
<Link href="/admin/gallery" className="tag-label hover:text-ink">
Gallery
</Link>
<Link href="/admin/change-password" className="tag-label hover:text-ink">
Change password
</Link>
</div>
);
}
+63
View File
@@ -0,0 +1,63 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
interface GalleryPhoto {
id: string;
imageDataUrl: string;
caption: string | null;
}
interface Props {
galleryPhotos: GalleryPhoto[];
}
export default function RandomGalleryHero({ galleryPhotos }: Props) {
const [photos, setPhotos] = useState<GalleryPhoto[]>([]);
useEffect(() => {
if (galleryPhotos.length === 0) {
setPhotos([]);
return;
}
// Pick 2 random photos
const shuffled = [...galleryPhotos].sort(() => Math.random() - 0.5);
setPhotos(shuffled.slice(0, 2));
// Rotate every 8 seconds
const interval = setInterval(() => {
const newShuffled = [...galleryPhotos].sort(() => Math.random() - 0.5);
setPhotos(newShuffled.slice(0, 2));
}, 8000);
return () => clearInterval(interval);
}, [galleryPhotos]);
if (photos.length === 0) {
return null;
}
return (
<div className="flex gap-4">
{photos.map((photo, idx) => (
<div key={photo.id} className={`flex-1 transition-opacity duration-500 ${idx === 1 ? 'translate-y-6' : ''}`}>
<div className="border-2 border-line rounded-lg overflow-hidden aspect-square">
<img
src={photo.imageDataUrl}
alt={photo.caption || 'Customer design'}
className="w-full h-full object-cover"
/>
</div>
{photo.caption && (
<p className="mt-3 text-xs text-muted text-center font-medium">{photo.caption}</p>
)}
{!photo.caption && (
<p className="mt-3 text-xs text-muted text-center font-medium">Customer work</p>
)}
</div>
))}
</div>
);
}