feat: Phase 0 – Gerüst (FastAPI+SQLite, Multi-User-Login, React-App-Shell, Deploy)
Grundgerüst für Certain Finance: - Backend: FastAPI + SQLAlchemy + SQLite, Alembic-Migrationen (Batch-Mode), argon2-Passwort-Hashing, server-seitige Sitzungen im Cookie, Erst-Admin, Benutzerverwaltung mit Temporär-Passwort, Einstellungen je Benutzer. - Ein Prozess, ein Port: uvicorn liefert API (/api) und gebautes SPA aus, inkl. SPA-Deeplink-Fallback. - Frontend: React 18 + Vite + JSX, Design-Tokens (Light/Dark), i18n DE/EN, Inline-SVG-Icons (keine Emojis), Login, Passwort-ändern, Dashboard-Platzhalter, Benutzer- und Einstellungsseiten. - Deploy: nativer LXC-Installer + Update-Skript (systemd), env-Vorlage, Doku. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+127
@@ -0,0 +1,127 @@
|
||||
import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "./auth";
|
||||
import Icon from "./components/Icon";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import BrandMark from "./components/BrandMark";
|
||||
import Login from "./pages/Login";
|
||||
import ChangePassword from "./pages/ChangePassword";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import Users from "./pages/Users";
|
||||
import Settings from "./pages/Settings";
|
||||
|
||||
const navClass = ({ isActive }) => (isActive ? "nav-link active" : "nav-link");
|
||||
|
||||
function NavItem({ to, icon, label, end }) {
|
||||
return (
|
||||
<NavLink to={to} end={end} className={navClass}>
|
||||
<Icon name={icon} size={17} />
|
||||
<span className="label">{label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar() {
|
||||
const { user, isAdmin, logout } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function handleLogout() {
|
||||
await logout();
|
||||
navigate("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-brand">
|
||||
<BrandMark />
|
||||
</div>
|
||||
<nav className="sidebar-nav">
|
||||
<NavItem to="/" end icon="dashboard" label={t("nav.overview")} />
|
||||
<NavItem to="/settings" icon="settings" label={t("nav.settings")} />
|
||||
{isAdmin && (
|
||||
<>
|
||||
<div className="nav-section">{t("nav.system")}</div>
|
||||
<NavItem to="/users" icon="users" label={t("nav.users")} />
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
<div className="sidebar-foot">
|
||||
<div className="sidebar-user">
|
||||
<span className="name">{user?.display_name || user?.username}</span>
|
||||
<span className="role">{isAdmin ? t("common.admin") : t("common.user")}</span>
|
||||
</div>
|
||||
<button className="btn-icon" onClick={handleLogout} title={t("common.logout")}>
|
||||
<Icon name="logout" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
/** Server nicht erreichbar – die Anmeldung bleibt bestehen, nur der Abruf nicht. */
|
||||
function StartFehler({ text, erneut }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="center" style={{ padding: 60 }}>
|
||||
<div className="alert error" style={{ maxWidth: 520 }}>
|
||||
<Icon name="alert" size={16} />
|
||||
<div>
|
||||
<div>{text}</div>
|
||||
<div className="muted small" style={{ marginTop: 4 }}>
|
||||
{t("server.sessionKept")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn primary" style={{ marginTop: 16 }} onClick={erneut}>
|
||||
{t("common.retry")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Protected({ children, adminOnly = false, wide = false }) {
|
||||
const { user, isAdmin, loading, startFehler, erneutVersuchen } = useAuth();
|
||||
const { pathname } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (loading) return <div className="center muted" style={{ padding: 60 }}>{t("common.loading")}</div>;
|
||||
if (startFehler) return <StartFehler text={startFehler} erneut={erneutVersuchen} />;
|
||||
if (!user) return <Navigate to="/login" replace />;
|
||||
// Vom Admin gesetztes Temporär-Passwort: erst ändern, dann weiter.
|
||||
if (user.must_change_password && pathname !== "/passwort-aendern") {
|
||||
return <Navigate to="/passwort-aendern" replace />;
|
||||
}
|
||||
if (adminOnly && !isAdmin) return <Navigate to="/" replace />;
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<Sidebar />
|
||||
<div className="main">
|
||||
<div className={wide ? "content content--wide" : "content"}>
|
||||
<ErrorBoundary key={pathname} bereich="Diese Seite">
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/login"
|
||||
element={loading ? null : user ? <Navigate to="/" replace /> : <Login />}
|
||||
/>
|
||||
<Route path="/passwort-aendern" element={<ChangePassword />} />
|
||||
<Route path="/" element={<Protected wide><Dashboard /></Protected>} />
|
||||
<Route path="/settings" element={<Protected><Settings /></Protected>} />
|
||||
<Route path="/users" element={<Protected adminOnly><Users /></Protected>} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user