Add curator authentication with audit logging and fix empty 3D gallery sessions.

Introduce session-based curator login, gate debug/checkup routes, log mutations to curator_audit_log, and keep guest hall preload public. Fix gallery view mounting so WebGL halls render reliably after navigation.
This commit is contained in:
Danila Khodjaef
2026-07-06 00:17:01 +03:00
parent aa31a2aa6e
commit 9da065acbe
27 changed files with 1252 additions and 112 deletions
+132
View File
@@ -0,0 +1,132 @@
.curator-login-backdrop {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.65);
padding: 16px;
}
.curator-login-modal {
width: min(100%, 360px);
padding: 24px;
border-radius: 10px;
border: 1px solid rgba(201, 169, 110, 0.35);
background: linear-gradient(180deg, #1a1a2e 0%, #12121f 100%);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55);
}
.curator-login-modal h2 {
margin: 0 0 8px;
font-family: 'Georgia', serif;
font-size: 20px;
color: #e8d5b5;
}
.curator-login-hint {
margin: 0 0 20px;
font-size: 13px;
color: rgba(201, 169, 110, 0.7);
line-height: 1.45;
}
.curator-login-field {
display: block;
margin-bottom: 14px;
}
.curator-login-field span {
display: block;
margin-bottom: 6px;
font-size: 12px;
color: rgba(232, 213, 181, 0.85);
}
.curator-login-field input {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid rgba(201, 169, 110, 0.35);
background: rgba(0, 0, 0, 0.35);
color: #e8d5b5;
font-size: 14px;
}
.curator-login-field input:focus {
outline: 2px solid rgba(255, 220, 160, 0.45);
outline-offset: 1px;
}
.curator-login-error {
margin: 0 0 12px;
font-size: 13px;
color: #f0a0a0;
}
.curator-login-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 8px;
}
.curator-login-cancel,
.curator-login-submit {
padding: 8px 14px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
}
.curator-login-cancel {
border: 1px solid rgba(201, 169, 110, 0.25);
background: transparent;
color: rgba(201, 169, 110, 0.8);
}
.curator-login-submit {
border: 1px solid rgba(255, 220, 160, 0.45);
background: rgba(232, 160, 64, 0.2);
color: #e8d5b5;
}
.curator-login-submit:disabled,
.curator-login-cancel:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.curator-login-gate {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px;
text-align: center;
background: linear-gradient(180deg, #0f0f1a 0%, #1a1a2e 40%, #16213e 100%);
color: rgba(201, 169, 110, 0.85);
}
.curator-login-gate h2 {
margin: 0;
font-family: 'Georgia', serif;
color: #e8d5b5;
}
.curator-login-gate p {
margin: 0;
max-width: 420px;
line-height: 1.5;
}
.curator-login-gate-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
@@ -0,0 +1,90 @@
import { useEffect, useState, type FormEvent } from 'react';
import './CuratorLoginModal.css';
interface Props {
open: boolean;
onClose: () => void;
onLogin: (username: string, password: string) => Promise<void>;
}
export default function CuratorLoginModal({ open, onClose, onLogin }: Props) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (!open) {
setUsername('');
setPassword('');
setError(null);
setSubmitting(false);
}
}, [open]);
if (!open) return null;
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setSubmitting(true);
try {
await onLogin(username.trim(), password);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed');
} finally {
setSubmitting(false);
}
};
return (
<div className="curator-login-backdrop" onMouseDown={onClose}>
<div
className="curator-login-modal"
role="dialog"
aria-labelledby="curator-login-title"
aria-modal="true"
onMouseDown={(e) => e.stopPropagation()}
>
<h2 id="curator-login-title">Curator login</h2>
<p className="curator-login-hint">
Debug tools and catalog edits require a curator account.
</p>
<form onSubmit={handleSubmit}>
<label className="curator-login-field">
<span>Username</span>
<input
type="text"
autoComplete="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={submitting}
required
/>
</label>
<label className="curator-login-field">
<span>Password</span>
<input
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={submitting}
required
/>
</label>
{error && <p className="curator-login-error">{error}</p>}
<div className="curator-login-actions">
<button type="button" className="curator-login-cancel" onClick={onClose} disabled={submitting}>
Cancel
</button>
<button type="submit" className="curator-login-submit" disabled={submitting}>
{submitting ? 'Signing in…' : 'Sign in'}
</button>
</div>
</form>
</div>
</div>
);
}