Initial release: self-hosted client photo gallery
ci / docker (push) Successful in 13m10s

Rust (axum + sqlx) API and worker sharing a Postgres-backed job queue
(SKIP LOCKED, heartbeat, reaper, typed statuses), S3 storage with derived
keys and a fully private bucket, OIDC photographer login with per-request
allowlist checks, client share links with argon2 passwords and lockout,
cookie-based image authorization with sliding expiry, hand-rolled
spec-compliant streaming ZIP downloads with exact Content-Length,
React + Vite gallery frontend, single Docker image, Helm chart for
external S3 + Postgres, and Gitea CI.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-17 13:12:42 +02:00
co-authored by Claude
commit f0238fc625
55 changed files with 11962 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
import { useEffect } from 'react'
import { imgUrl } from '../api'
export default function Lightbox({ photos, index, onClose, onNav, footer }) {
const photo = photos[index]
useEffect(() => {
const onKey = (e) => {
if (e.key === 'Escape') onClose()
if (e.key === 'ArrowRight' && index < photos.length - 1) onNav(index + 1)
if (e.key === 'ArrowLeft' && index > 0) onNav(index - 1)
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [index, photos.length, onClose, onNav])
useEffect(() => {
document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = ''
}
}, [])
if (!photo) return null
return (
<div className="lightbox" onClick={onClose}>
<div className="lb-top" onClick={(e) => e.stopPropagation()}>
<span className="lb-name">{photo.filename}</span>
<span className="lb-count">
{index + 1} / {photos.length}
</span>
<button className="lb-btn" onClick={onClose} title="Close (Esc)">
</button>
</div>
<button
className="lb-nav lb-prev"
disabled={index === 0}
onClick={(e) => {
e.stopPropagation()
onNav(index - 1)
}}
>
</button>
<div className="lb-stage">
<img
className="lb-img"
src={imgUrl(photo, 'preview')}
alt={photo.filename}
onClick={(e) => e.stopPropagation()}
/>
</div>
<button
className="lb-nav lb-next"
disabled={index === photos.length - 1}
onClick={(e) => {
e.stopPropagation()
onNav(index + 1)
}}
>
</button>
{footer && (
<div className="lb-footer" onClick={(e) => e.stopPropagation()}>
{footer(photo)}
</div>
)}
</div>
)
}