00f07d76f6
Sicherheits-Hardening - Passwort-Hashing mit PBKDF2 (SHA-256, 100k Iterationen) inkl. transparenter Migration bestehender Klartext-Passwörter beim ersten Login - Login Brute-Force-Schutz (5 Fehlversuche → 60s Lockout), Constant-Time-Compare, Mindestpasswortlänge 8 Zeichen - HTML-Sanitizer für Brieftexte (Allowlist, entfernt javascript:/data:/vbscript:-URLs, Event-Handler, Script-Tags; rel=noopener für target=_blank) - Datenexport entfernt Legacy-Klartextpasswörter (Hashes bleiben) - Kryptografische IDs via crypto.randomUUID statt Math.random - sessionStorage speichert keine Credentials mehr GUI & Performance - Code-Splitting pro View via React.lazy + Suspense (Initial-Bundle 86 KB gzipped) - swissqrbill als lokale Dependency — QR-Rechnungen offline-fähig - Spesenbelege (Bild/PDF) direkt in der Tageserfassung mit Bildkomprimierung - Avatar-Upload: 256px-Skalierung + JPEG-Kompression, Typprüfung - Über-Rapport-Modal, einheitliche Bearbeiten-Icons, Pinnwand-Kategorien als Pills Bug-Fixes - Auto-überfällig-Routine läuft nur noch einmal pro Tag (verhindert Re-Render-Loop) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
142 lines
6.2 KiB
React
142 lines
6.2 KiB
React
import React, { useState } from "react";
|
|
import Setup from "./Setup.jsx";
|
|
|
|
export default function MigrationScreen({ data, onComplete }) {
|
|
const [backed, setBacked] = useState(false);
|
|
const [goSetup, setGoSetup] = useState(false);
|
|
|
|
const studioName = data.settings?.name || "Studio";
|
|
|
|
if (goSetup) {
|
|
return <Setup onComplete={onComplete} />;
|
|
}
|
|
|
|
const handleBackup = () => {
|
|
try {
|
|
const stored = localStorage.getItem("studio_data_v1") || JSON.stringify(data);
|
|
const blob = new Blob([stored], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `rapport-backup-${new Date().toISOString().split("T")[0]}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
} catch {}
|
|
setBacked(true);
|
|
};
|
|
|
|
return (
|
|
<div style={{
|
|
minHeight: "100vh", minWidth: "100vw",
|
|
background: "#ebe7e1",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
fontFamily: "'DM Mono', 'Courier New', monospace",
|
|
position: "fixed", inset: 0, zIndex: 9999,
|
|
overflowY: "auto", padding: "24px 0",
|
|
}}>
|
|
<style>{`
|
|
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:wght@300;400;500&display=swap');
|
|
@keyframes mig-fade-in {
|
|
from { opacity: 0; transform: translateY(16px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.mig-card { animation: mig-fade-in 0.5s cubic-bezier(0.22,1,0.36,1) both; }
|
|
.mig-btn-primary {
|
|
width: 100%; padding: 13px;
|
|
background: #1a1a18; color: #f0ede8;
|
|
border: none; border-radius: 10px;
|
|
font-family: 'DM Mono', monospace; font-size: 13px;
|
|
font-weight: 500; letter-spacing: 0.04em;
|
|
cursor: pointer; transition: background 0.18s;
|
|
}
|
|
.mig-btn-primary:hover { background: #2e2e28; }
|
|
.mig-btn-backup {
|
|
width: 100%; padding: 13px;
|
|
background: transparent; color: #1a1a18;
|
|
border: 2px solid #1a1a18; border-radius: 10px;
|
|
font-family: 'DM Mono', monospace; font-size: 13px;
|
|
font-weight: 500; letter-spacing: 0.04em;
|
|
cursor: pointer; transition: background 0.18s;
|
|
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
}
|
|
.mig-btn-backup:hover { background: #f0ece6; }
|
|
.mig-btn-backup.done { border-color: #2d6a4f; color: #2d6a4f; background: #eaf5ee; cursor: default; }
|
|
.mig-skip { background: none; border: none; font-family: inherit; font-size: 11px; color: #aaa; cursor: pointer; text-decoration: underline; padding: 0; }
|
|
.mig-skip:hover { color: #888; }
|
|
`}</style>
|
|
|
|
<div className="mig-card" style={{
|
|
background: "#fdfcfa",
|
|
borderRadius: 20,
|
|
padding: "44px 40px 36px",
|
|
width: "100%", maxWidth: 440,
|
|
boxShadow: "0 8px 40px rgba(0,0,0,0.10), 0 2px 8px rgba(0,0,0,0.07)",
|
|
border: "1px solid #ddd8d0",
|
|
margin: "0 20px",
|
|
}}>
|
|
{/* Header */}
|
|
<div style={{ textAlign: "center", marginBottom: 28 }}>
|
|
<div style={{ fontFamily: "Krungthep, 'Archivo Black', sans-serif", fontSize: 30, color: "#1a1a18", letterSpacing: "-0.02em", lineHeight: 1 }}>
|
|
RAPPORT
|
|
</div>
|
|
<div style={{ fontSize: 9, color: "#b0aca4", letterSpacing: "0.2em", marginTop: 6, fontWeight: 500 }}>
|
|
{studioName.toUpperCase()}
|
|
</div>
|
|
<div style={{ width: 32, height: 1.5, background: "#ddd8d0", margin: "14px auto 0" }} />
|
|
</div>
|
|
|
|
{/* Info banner */}
|
|
<div style={{ padding: "14px 16px", background: "#fff8ed", borderRadius: 10, border: "1px solid #f0e4c4", marginBottom: 10 }}>
|
|
<div style={{ fontSize: 11, fontWeight: 600, color: "#b07848", letterSpacing: "0.06em", marginBottom: 6 }}>
|
|
VERSION 0.5 — NEUE INITIALISIERUNG
|
|
</div>
|
|
<div style={{ fontSize: 12, color: "#7a5a30", lineHeight: 1.65 }}>
|
|
Diese Version enthält umfangreiche Datenbankänderungen und ein neues Anmeldesystem. Die App muss neu eingerichtet werden.
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recommendation */}
|
|
<div style={{ padding: "12px 16px", background: "#f5f0e8", borderRadius: 10, border: "1px solid #e0d8cc", marginBottom: 24, fontSize: 12, color: "#5a5040", lineHeight: 1.65 }}>
|
|
<strong style={{ display: "block", marginBottom: 4, fontSize: 11, letterSpacing: "0.06em", color: "#1a1a18" }}>EMPFEHLUNG</strong>
|
|
Sichern Sie zuerst die bestehenden Daten. Alte Datenbankstrukturen sind zwar importierbar, das Einspielen von Backups zum Testen wird jedoch ausdrücklich abgeraten — umbenannte Felder können zu Fehlern führen.
|
|
</div>
|
|
|
|
{/* Step 1: Backup */}
|
|
<div style={{ marginBottom: 6 }}>
|
|
<div style={{ fontSize: 9, letterSpacing: "0.15em", color: "#8c8880", marginBottom: 8, fontWeight: 500 }}>
|
|
SCHRITT 1 — DATEN SICHERN
|
|
</div>
|
|
<button
|
|
className={`mig-btn-backup${backed ? " done" : ""}`}
|
|
onClick={backed ? undefined : handleBackup}
|
|
>
|
|
{backed
|
|
? <><span>✓</span> Backup heruntergeladen</>
|
|
: <><span style={{ fontSize: 16 }}>↓</span> Datenbank herunterladen</>
|
|
}
|
|
</button>
|
|
{!backed && (
|
|
<div style={{ textAlign: "right", marginTop: 6 }}>
|
|
<button className="mig-skip" onClick={() => setBacked(true)}>
|
|
Kein Backup nötig — überspringen
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Step 2: Setup */}
|
|
<div style={{ marginTop: 20, opacity: backed ? 1 : 0.3, transition: "opacity 0.3s", pointerEvents: backed ? "auto" : "none" }}>
|
|
<div style={{ fontSize: 9, letterSpacing: "0.15em", color: "#8c8880", marginBottom: 8, fontWeight: 500 }}>
|
|
SCHRITT 2 — NEU EINRICHTEN
|
|
</div>
|
|
<button className="mig-btn-primary" onClick={() => setGoSetup(true)}>
|
|
Weiter zur Einrichtung
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|