Convert Server from submodule to normal files
This commit is contained in:
376
Server/src/App.jsx
Normal file
376
Server/src/App.jsx
Normal 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 },
|
||||
};
|
||||
Reference in New Issue
Block a user