"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"; import { fetchManagedAccounts } from "./services/accountsService"; import { useScan } from "./context/ScanContext"; import ScanningLoader from "./components/ui/ScanningLoader"; export default function App() { const { route, navigate } = useHashRoute(); const { scanning, scanProgress } = useScan(); 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 [userExtensionLoad, setUserExtensionLoad] = useState(null); // null = nicht geprüft, true/false = geprüft const [hasAccounts, setHasAccounts] = useState(false); // true wenn User Accounts hat // Gate soll angezeigt werden, wenn: // 1. User-Dokument nicht existiert ODER // 2. User-Dokument existiert, aber user_extension_load = false UND keine Accounts vorhanden // Gate wird versteckt, wenn: // - User-Dokument existiert UND (user_extension_load = true ODER Accounts vorhanden) const showGate = (!hasUserDoc || (hasUserDoc && userExtensionLoad === false && !hasAccounts)) && !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); // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:68',message:'refreshAuth: userDocExists check',data:{userId:user.$id,userDocExists},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{}); // #endregion setHasUserDoc(userDocExists); // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:71',message:'refreshAuth: hasUserDoc set',data:{hasUserDoc:userDocExists,showGateWillBe:!userDocExists && !checkingUserDoc && user !== null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{}); // #endregion // Prüfe user_extension_load und Accounts, wenn User-Dokument existiert if (userDocExists && databases && databaseId && usersCollectionId) { try { const userDoc = await databases.getDocument(databaseId, usersCollectionId, user.$id); const extensionLoad = userDoc?.user_extension_load === true; // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:78',message:'refreshAuth: userDoc retrieved for extension check',data:{userExtensionLoad:userDoc?.user_extension_load,userExtensionLoadType:typeof userDoc?.user_extension_load,userExtensionLoadStrictFalse:userDoc?.user_extension_load === false,extensionLoad},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{}); // #endregion setUserExtensionLoad(extensionLoad); // Prüfe auch Accounts try { const existingAccounts = await fetchManagedAccounts(user.$id); const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0; setHasAccounts(hasAccountsValue); } catch (accountsErr) { setHasAccounts(false); } } catch (docErr) { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:82',message:'refreshAuth: error getting userDoc for extension check',data:{error:docErr.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{}); // #endregion // Bei Fehler: Setze auf null (nicht geprüft) setUserExtensionLoad(null); setHasAccounts(false); } } else { // Kein User-Dokument → user_extension_load ist nicht relevant setUserExtensionLoad(null); setHasAccounts(false); } await handoffJwtToExtension(); } catch (e) { setStatus({ loading: false, authed: false, error: "" }); setAuthUser(null); setHasUserDoc(false); setUserExtensionLoad(null); setHasAccounts(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); // Prüfe user_extension_load und Accounts, wenn User-Dokument existiert if (userDocExists && databases && databaseId && usersCollectionId) { try { const userDoc = await databases.getDocument(databaseId, usersCollectionId, user.$id); const extensionLoad = userDoc?.user_extension_load === true; setUserExtensionLoad(extensionLoad); // Prüfe auch Accounts try { const existingAccounts = await fetchManagedAccounts(user.$id); const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0; setHasAccounts(hasAccountsValue); } catch (accountsErr) { setHasAccounts(false); } } catch (docErr) { setUserExtensionLoad(null); setHasAccounts(false); } } else { setUserExtensionLoad(null); setHasAccounts(false); } await handoffJwtToExtension(); } catch (e) { setStatus({ loading: false, authed: false, error: "Login fehlgeschlagen" }); setAuthUser(null); setHasUserDoc(false); setUserExtensionLoad(null); setHasAccounts(false); } finally { setCheckingUserDoc(false); } } async function handleOnboardingStart() { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:162',message:'handleOnboardingStart: entry',data:{hasAuthUser:!!authUser,hasDatabases:!!databases,hasDatabaseId:!!databaseId,hasUsersCollectionId:!!usersCollectionId},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion if (!authUser || !databases || !databaseId || !usersCollectionId) { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:165',message:'handleOnboardingStart: configuration incomplete',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion setOnboardingError("Fehler: Konfiguration unvollständig"); return; } setOnboardingLoading(true); setOnboardingError(""); try { // Prüfe zuerst, ob User-Dokument bereits existiert let userDocExists = false; try { await databases.getDocument(databaseId, usersCollectionId, authUser.$id); userDocExists = true; // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:178',message:'handleOnboardingStart: userDoc exists',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion } catch (e) { // Dokument existiert nicht - wird erstellt // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:182',message:'handleOnboardingStart: userDoc does not exist, will create',data:{error:e.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion } if (!userDocExists) { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:187',message:'handleOnboardingStart: creating userDoc',data:{userId:authUser.$id},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion await databases.createDocument( databaseId, usersCollectionId, authUser.$id, // Document-ID = Auth-User-ID { user_name: authUser.name || "User", user_extension_load: false } ); // Erfolg: User-Dokument erstellt setHasUserDoc(true); // user_extension_load ist false beim Erstellen (siehe payload) setUserExtensionLoad(false); // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:202',message:'handleOnboardingStart: userDoc created, setting hasUserDoc=true',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion // Prüfe, ob User bereits Accounts hat (nach dem Erstellen des User-Dokuments) // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:207',message:'handleOnboardingStart: checking for accounts after userDoc creation',data:{userId:authUser.$id},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion try { const existingAccounts = await fetchManagedAccounts(authUser.$id); const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0; setHasAccounts(hasAccountsValue); // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:212',message:'handleOnboardingStart: accounts check result after creation',data:{hasAccounts:hasAccountsValue,accountsCount:existingAccounts?.length || 0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion } catch (accountsErr) { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:216',message:'handleOnboardingStart: error checking accounts after creation',data:{error:accountsErr.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion setHasAccounts(false); } } else { // Dokument existiert bereits - prüfe user_extension_load // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:206',message:'handleOnboardingStart: userDoc exists, checking extension load',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion const userDoc = await databases.getDocument(databaseId, usersCollectionId, authUser.$id); const extensionLoad = userDoc?.user_extension_load === true; setHasUserDoc(true); setUserExtensionLoad(extensionLoad); // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:212',message:'handleOnboardingStart: userDoc retrieved, setting hasUserDoc=true',data:{extensionLoad},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion } // Prüfe, ob User Accounts hat (nach dem Erstellen/Prüfen des User-Dokuments) // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:217',message:'handleOnboardingStart: checking for accounts',data:{userId:authUser.$id},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion try { const existingAccounts = await fetchManagedAccounts(authUser.$id); const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0; setHasAccounts(hasAccountsValue); // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:222',message:'handleOnboardingStart: accounts check result',data:{hasAccounts:hasAccountsValue,accountsCount:existingAccounts?.length || 0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion } catch (accountsErr) { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:226',message:'handleOnboardingStart: error checking accounts',data:{error:accountsErr.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion setHasAccounts(false); } setOnboardingError(""); // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:232',message:'handleOnboardingStart: success, showGate will be',data:{hasUserDoc:true,userExtensionLoad,hasAccounts,showGateWillBe:(hasUserDoc && userExtensionLoad === false && !hasAccounts)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion } catch (e) { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:220',message:'handleOnboardingStart: error caught',data:{errorCode:e.code,errorType:e.type,errorMessage:e.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion // 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') { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:225',message:'handleOnboardingStart: document already exists (409)',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion setHasUserDoc(true); // Prüfe user_extension_load für existierendes Dokument if (authUser && databases && databaseId && usersCollectionId) { try { const userDoc = await databases.getDocument(databaseId, usersCollectionId, authUser.$id); const extensionLoad = userDoc?.user_extension_load === true; setUserExtensionLoad(extensionLoad); // Prüfe auch Accounts try { const existingAccounts = await fetchManagedAccounts(authUser.$id); const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0; setHasAccounts(hasAccountsValue); } catch (accountsErr) { setHasAccounts(false); } } catch (docErr) { setUserExtensionLoad(null); setHasAccounts(false); } } else { setUserExtensionLoad(null); setHasAccounts(false); } 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 { // #region agent log fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:245',message:'handleOnboardingStart: finally, setting loading=false',data:{hasUserDoc,userExtensionLoad,showGateWillBe:(!hasUserDoc || (hasUserDoc && userExtensionLoad === false)) && !checkingUserDoc && authUser !== null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{}); // #endregion 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); setUserExtensionLoad(null); setHasAccounts(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: ( ), onClick: (e) => { e.preventDefault(); navigate("/"); }, }, { label: "Accounts", href: "#/accounts", icon: ( ), disabled: scanning, onClick: (e) => { e.preventDefault(); navigate("/accounts"); }, }, { label: "Profile", href: "#", icon: ( ), }, { label: "Settings", href: "#", icon: ( ), }, { label: "Logout", href: "#", icon: ( ), onClick: (e) => { e.preventDefault(); logout(); }, }, ]; // Rendere Content basierend auf Route const renderContent = () => { if (route === "/accounts") { return ; } // Default: Dashboard return ; }; return (
{!status.authed && (
Login
Appwrite Session erforderlich
setEmail(e.target.value)} autoComplete="email" /> setPassword(e.target.value)} autoComplete="current-password" /> {status.error &&
{status.error}
}
Nach Login wird der Sperrbildschirm entfernt und die Extension erhaelt ein JWT.
)} {status.authed && ( <> {showGate && ( )}
{!scanning && (
{links.map((link, idx) => ( ))}
{ e.preventDefault(); logout(); }} />
)} {renderContent()}
{scanning && ( )}
)}
); } 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 }, };