Convert Server from submodule to normal files

This commit is contained in:
2026-01-18 17:50:49 +01:00
parent 86d2191a25
commit 0012a10249
52 changed files with 11975 additions and 1 deletions

376
Server/src/App.jsx Normal file
View File

@@ -0,0 +1,376 @@
"use client";
import React, { useEffect, useState } from "react";
import { Sidebar, SidebarBody, SidebarLink } from "./components/sidebar";
import {
IconArrowLeft,
IconBrandTabler,
IconSettings,
IconUserBolt,
IconShoppingBag,
} from "@tabler/icons-react";
import { motion } from "motion/react";
import { cn } from "./lib/utils";
import { BackgroundRippleEffect } from "./components/layout/BackgroundRippleEffect";
import { Dashboard } from "./components/dashboard/Dashboard";
import { AccountsPage } from "./pages/AccountsPage";
import LogoutButton from "./components/ui/LogoutButton";
import { OnboardingGate } from "./components/onboarding/OnboardingGate";
import { SidebarHeader } from "./components/sidebar/SidebarHeader";
import { useHashRoute } from "./lib/routing";
import { account, databases, databaseId, usersCollectionId } from "./lib/appwrite";
export default function App() {
const { route, navigate } = useHashRoute();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [status, setStatus] = useState({ loading: true, authed: false, error: "" });
const [sidebarOpen, setSidebarOpen] = useState(false);
// Gate-Management State
const [authUser, setAuthUser] = useState(null);
const [hasUserDoc, setHasUserDoc] = useState(false);
const [checkingUserDoc, setCheckingUserDoc] = useState(false);
const [onboardingLoading, setOnboardingLoading] = useState(false);
const [onboardingError, setOnboardingError] = useState("");
const showGate = !hasUserDoc && !checkingUserDoc && authUser !== null;
async function checkUserDocument(userId) {
if (!databases || !databaseId || !usersCollectionId) {
return false;
}
try {
await databases.getDocument(databaseId, usersCollectionId, userId);
return true; // Dokument existiert
} catch (e) {
// 404 bedeutet, dass das Dokument nicht existiert - das ist OK
if (e.code === 404 || e.type === 'document_not_found') {
return false; // Dokument existiert nicht
}
// Andere Fehler: Loggen, aber nicht als kritischen Fehler behandeln
console.warn('Fehler beim Prüfen des User-Dokuments:', e.message || e);
return false; // Im Zweifel als "nicht vorhanden" behandeln
}
}
async function refreshAuth() {
setStatus((s) => ({ ...s, loading: true, error: "" }));
setCheckingUserDoc(true);
setOnboardingError("");
try {
const user = await account.get(); // wirft Fehler, wenn keine Session
setAuthUser(user);
setStatus({ loading: false, authed: true, error: "" });
// Prüfe, ob User-Dokument existiert
const userDocExists = await checkUserDocument(user.$id);
setHasUserDoc(userDocExists);
await handoffJwtToExtension();
} catch (e) {
setStatus({ loading: false, authed: false, error: "" });
setAuthUser(null);
setHasUserDoc(false);
} finally {
setCheckingUserDoc(false);
}
}
async function login(e) {
e.preventDefault();
setStatus((s) => ({ ...s, loading: true, error: "" }));
setCheckingUserDoc(true);
setOnboardingError("");
try {
await account.createEmailPasswordSession(email, password);
const user = await account.get();
setAuthUser(user);
setStatus({ loading: false, authed: true, error: "" });
// Prüfe, ob User-Dokument existiert
const userDocExists = await checkUserDocument(user.$id);
setHasUserDoc(userDocExists);
await handoffJwtToExtension();
} catch (e) {
setStatus({ loading: false, authed: false, error: "Login fehlgeschlagen" });
setAuthUser(null);
setHasUserDoc(false);
} finally {
setCheckingUserDoc(false);
}
}
async function handleOnboardingStart() {
if (!authUser || !databases || !databaseId || !usersCollectionId) {
setOnboardingError("Fehler: Konfiguration unvollständig");
return;
}
setOnboardingLoading(true);
setOnboardingError("");
try {
await databases.createDocument(
databaseId,
usersCollectionId,
authUser.$id, // Document-ID = Auth-User-ID
{
user_name: authUser.name || "User"
}
);
// Erfolg: User-Dokument erstellt
setHasUserDoc(true);
setOnboardingError("");
} catch (e) {
// 409 Conflict bedeutet, dass das Dokument bereits existiert
// Das ist ok, da wir idempotent sein wollen
if (e.code === 409 || e.type === 'document_already_exists') {
setHasUserDoc(true);
setOnboardingError("");
} else if (e.code === 401 || e.type === 'general_unauthorized_scope') {
// 401 Unauthorized: Permissions nicht richtig gesetzt
setOnboardingError(
"Berechtigung verweigert. Bitte prüfe in Appwrite, ob die users Collection die richtigen Permissions hat. " +
"Siehe setup/USERS_COLLECTION_SETUP.md für Details."
);
} else {
// Andere Fehler anzeigen
setOnboardingError(e.message || "Fehler beim Erstellen des Profils. Bitte versuche es erneut.");
}
} finally {
setOnboardingLoading(false);
}
}
async function logout() {
setStatus((s) => ({ ...s, loading: true, error: "" }));
try {
await account.deleteSession("current");
} catch {}
setStatus({ loading: false, authed: false, error: "" });
setAuthUser(null);
setHasUserDoc(false);
setOnboardingError("");
// Extension informieren: Token weg
sendToExtension({ type: "AUTH_CLEARED" });
}
async function handoffJwtToExtension() {
// JWT ist userbezogen und kann serverseitig validiert werden
const jwt = await account.createJWT();
sendToExtension({ type: "AUTH_JWT", jwt: jwt.jwt });
}
function sendToExtension(payload) {
// Sende Nachricht über window.postMessage (keine Extension ID nötig)
// Das Content Script der Extension lauscht darauf
window.postMessage(
{
source: "eship-webapp",
...payload,
},
"*"
);
}
useEffect(() => {
refreshAuth();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const links = [
{
label: "Dashboard",
href: "#/",
icon: (
<IconBrandTabler className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
onClick: (e) => {
e.preventDefault();
navigate("/");
},
},
{
label: "Accounts",
href: "#/accounts",
icon: (
<IconShoppingBag className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
onClick: (e) => {
e.preventDefault();
navigate("/accounts");
},
},
{
label: "Profile",
href: "#",
icon: (
<IconUserBolt className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
},
{
label: "Settings",
href: "#",
icon: (
<IconSettings className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
},
{
label: "Logout",
href: "#",
icon: (
<IconArrowLeft className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
onClick: (e) => {
e.preventDefault();
logout();
},
},
];
// Rendere Content basierend auf Route
const renderContent = () => {
if (route === "/accounts") {
return <AccountsPage />;
}
// Default: Dashboard
return <Dashboard />;
};
return (
<div style={styles.page}>
{!status.authed && (
<div style={styles.lockOverlay}>
<div style={styles.card}>
<div style={styles.title}>Login</div>
<div style={styles.sub}>Appwrite Session erforderlich</div>
<form onSubmit={login} style={styles.form}>
<input
style={styles.input}
placeholder="E-Mail"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
/>
<input
style={styles.input}
placeholder="Passwort"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
/>
<button style={styles.button} disabled={status.loading}>
{status.loading ? "..." : "Anmelden"}
</button>
{status.error && <div style={styles.error}>{status.error}</div>}
</form>
<div style={styles.hint}>
Nach Login wird der Sperrbildschirm entfernt und die Extension erhaelt ein JWT.
</div>
</div>
</div>
)}
{status.authed && (
<>
{showGate && (
<OnboardingGate
userName={authUser?.name || "User"}
onStart={handleOnboardingStart}
loading={onboardingLoading}
error={onboardingError}
/>
)}
<div style={{ display: showGate ? "none" : "block" }}>
<BackgroundRippleEffect />
<div
className={cn(
"flex w-full flex-1 flex-col overflow-hidden rounded-md border border-neutral-200 bg-gray-100 md:flex-row dark:border-neutral-700 dark:bg-neutral-800",
"h-screen relative z-10"
)}>
<Sidebar open={sidebarOpen} setOpen={setSidebarOpen} animate={true}>
<SidebarBody className="justify-between gap-10">
<div className="flex flex-1 flex-col overflow-x-hidden overflow-y-auto">
<SidebarHeader />
<div className="mt-8 flex flex-col gap-2">
{links.map((link, idx) => (
<SidebarLink key={idx} link={link} />
))}
</div>
</div>
<div>
<LogoutButton onClick={(e) => {
e.preventDefault();
logout();
}} />
</div>
</SidebarBody>
</Sidebar>
{renderContent()}
</div>
</div>
</>
)}
</div>
);
}
const styles = {
page: {
minHeight: "100vh",
background: "#0b0f19",
color: "#e7aaf0",
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif",
},
lockOverlay: {
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.70)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 18,
zIndex: 9999,
},
card: {
width: "100%",
maxWidth: 420,
background: "rgba(255,255,255,0.07)",
border: "1px solid rgba(255,255,255,0.12)",
borderRadius: 18,
padding: 20,
backdropFilter: "blur(10px)",
},
title: { fontSize: 20, fontWeight: 800 },
sub: { marginTop: 6, opacity: 0.8, fontSize: 13 },
form: { marginTop: 14, display: "grid", gap: 10 },
input: {
height: 42,
borderRadius: 12,
border: "1px solid rgba(255,255,255,0.16)",
outline: "none",
padding: "0 12px",
background: "rgba(0,0,0,0.20)",
color: "#e7aaf0",
},
button: {
height: 42,
borderRadius: 12,
border: "0",
cursor: "pointer",
fontWeight: 800,
background: "#e7aaf0",
color: "#0b0f19",
},
error: { marginTop: 6, color: "#ffb3b3", fontSize: 13 },
hint: { marginTop: 12, opacity: 0.75, fontSize: 12, lineHeight: 1.4 },
};