diff --git a/CLAUDE.md b/CLAUDE.md index cb3e254..a88eebc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,18 @@ Jeder Ordner hat eine eigene CLAUDE.md als Single Source of Truth: --- +## Auth-Status (Kurzüberblick) + +- Better Auth ist aktiv (Convex-Integration). +- Login unterstützt: + - E-Mail + Passwort + - Magic Link (Better-Auth Plugin) +- Details und Caveats (inkl. `SITE_URL`/`APP_URL`-Origin-Thema) stehen in: + - `convex/CLAUDE.md` + - `app/CLAUDE.md` + +--- + ## Design Context ### Users diff --git a/app/CLAUDE.md b/app/CLAUDE.md index 668e915..f826aad 100644 --- a/app/CLAUDE.md +++ b/app/CLAUDE.md @@ -66,6 +66,8 @@ Server Component. Initialisiert: - Client-Helper: `authClient` aus `lib/auth-client.ts` - Convex-Integration: `convex/auth.config.ts` + `convex/auth.ts` - Trusted Origins: `https://app.lemonspace.io`, `http://localhost:3000` +- Sign-In unterstützt `email+password` **und** `magic link` (`authClient.signIn.magicLink`) +- Dashboard-Route (`app/dashboard/page.tsx`) redirectet bei fehlender Session explizit nach `/auth/sign-in`, damit kein permanenter Loading-State entsteht --- diff --git a/components/dashboard/CLAUDE.md b/components/dashboard/CLAUDE.md index d765374..7de4d38 100644 --- a/components/dashboard/CLAUDE.md +++ b/components/dashboard/CLAUDE.md @@ -8,7 +8,7 @@ UI-Komponenten für die Startseite nach dem Login. | Datei | Zweck | |-------|-------| -| `canvas-card.tsx` | Karte für einen Canvas in der Übersicht (Thumbnail, Name, Datum) | +| `canvas-card.tsx` | Karte für einen Canvas in der Übersicht (Navigation, Umbenennen, Löschen mit Confirm-Dialog) | | `credit-overview.tsx` | Monatsverbrauch und verfügbare Credits als Balken-Visualisierung | | `recent-transactions.tsx` | Liste der letzten Credit-Transaktionen | @@ -24,6 +24,12 @@ Alle Daten kommen aus Convex-Queries via `useAuthQuery` (aus `hooks/use-auth-que | `credit-overview.tsx` | `api.credits.getBalance`, `api.credits.getUsageStats` | | `recent-transactions.tsx` | `api.credits.getRecentTransactions` | +## Mutations + +| Komponente | Mutation | +|-----------|----------| +| `canvas-card.tsx` | `api.canvases.update`, `api.canvases.remove` | + --- ## Layout-Seite diff --git a/components/dashboard/canvas-card.tsx b/components/dashboard/canvas-card.tsx index 59e68d9..e3a833e 100644 --- a/components/dashboard/canvas-card.tsx +++ b/components/dashboard/canvas-card.tsx @@ -2,15 +2,23 @@ import { useState, useCallback, useRef } from "react"; import { useMutation } from "convex/react"; -import { ArrowUpRight, MoreHorizontal, Pencil } from "lucide-react"; +import { ArrowUpRight, MoreHorizontal, Pencil, Trash2 } from "lucide-react"; import { toast } from "@/lib/toast"; import { msg } from "@/lib/toast-messages"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; @@ -31,6 +39,9 @@ export default function CanvasCard({ canvas, onNavigate }: CanvasCardProps) { const suppressCardNavigationRef = useRef(false); const saveInFlightRef = useRef(false); const updateCanvas = useMutation(api.canvases.update); + const removeCanvas = useMutation(api.canvases.remove); + const [deleteOpen, setDeleteOpen] = useState(false); + const [deleteBusy, setDeleteBusy] = useState(false); const handleStartEdit = useCallback(() => { suppressCardNavigationRef.current = true; @@ -98,66 +109,120 @@ export default function CanvasCard({ canvas, onNavigate }: CanvasCardProps) { } }, [isEditing, onNavigate, canvas._id]); + const handleDelete = useCallback(async () => { + setDeleteBusy(true); + try { + await removeCanvas({ canvasId: canvas._id }); + toast.success(msg.dashboard.deleteSuccess.title); + setDeleteOpen(false); + } catch { + toast.error(msg.dashboard.deleteFailed.title); + } finally { + setDeleteBusy(false); + } + }, [canvas._id, removeCanvas]); + return ( -
- {/* Avatar */} -
- {canvas.name.slice(0, 1).toUpperCase()} -
- - {/* Content */} -
- {isEditing ? ( - setEditName(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleBlur} - disabled={isSaving} - autoFocus - onClick={(e) => e.stopPropagation()} - className="h-auto py-0.5 text-sm font-medium bg-transparent border px-1.5 focus-visible:ring-1" - /> - ) : ( -

{canvas.name}

+ <> +
+ {/* Avatar */} +
+ {canvas.name.slice(0, 1).toUpperCase()} +
+ + {/* Content */} +
+ {isEditing ? ( + setEditName(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleBlur} + disabled={isSaving} + autoFocus + onClick={(e) => e.stopPropagation()} + className="h-auto border bg-transparent px-1.5 py-0.5 text-sm font-medium focus-visible:ring-1" + /> + ) : ( +

{canvas.name}

+ )} +

Canvas

+
+ + {/* Actions - positioned to not overlap with content */} + {!isEditing && ( +
+ + + + + + + + + + Umbenennen + + + { + setDeleteOpen(true); + }} + > + + Löschen + + + +
)} -

Canvas

- {/* Actions - positioned to not overlap with content */} - {!isEditing && ( -
- - - - - - - - - - Umbenennen - - - -
- )} -
+ + + + Arbeitsbereich löschen? + +

+ „{canvas.name}“ und alle Knoten werden dauerhaft gelöscht. Das + lässt sich nicht rückgängig machen. +

+ + + + +
+
+ ); } diff --git a/convex/CLAUDE.md b/convex/CLAUDE.md index 585bfcc..6380942 100644 --- a/convex/CLAUDE.md +++ b/convex/CLAUDE.md @@ -116,6 +116,20 @@ optionalAuth(ctx) // → { userId: string } | null Wirft bei unauthentifiziertem Zugriff. Wird von allen Queries und Mutations genutzt, die User-Daten berühren. +### Better-Auth Setup (`auth.ts`) + +- Auth-Library läuft in Convex (`createAuth`) und wird via `authComponent.registerRoutes(http, createAuth)` in `http.ts` registriert. +- Login-Modi: + - `emailAndPassword` (mit `requireEmailVerification: true`) + - `magicLink` Plugin (`better-auth/plugins`) +- Magic-Link-Konfiguration: + - `disableSignUp: true` (Magic Link nur für bestehende Accounts) + - `expiresIn: 600` (10 Minuten) + - Versand über Resend (`sendMagicLink`) +- **Wichtig für Multi-Domain-Setup (`SITE_URL` + `APP_URL`)**: + - Verify-Links aus Better Auth werden vor dem Versand auf die App-Origin umgeschrieben (`toAuthAppUrl(...)`), damit Session-Cookies auf der korrekten Origin gesetzt werden. + - Ohne dieses Umschreiben kann Login per Magic Link zwar erfolgreich sein, aber das Dashboard in einem permanenten Loading-State hängen (fehlende Session auf App-Origin). + ### Auth-Race-Härtung - `canvases.get` nutzt optionalen Auth-Check und gibt bei fehlender Session `null` zurück (statt Throw), damit SSR/Client-Hydration bei kurzem Token-Race nicht in `404` kippt. diff --git a/lib/toast-messages.ts b/lib/toast-messages.ts index c022aa3..ade39e4 100644 --- a/lib/toast-messages.ts +++ b/lib/toast-messages.ts @@ -188,5 +188,7 @@ export const msg = { renameEmpty: { title: "Name ungültig", desc: "Name darf nicht leer sein." }, renameSuccess: { title: "Arbeitsbereich umbenannt" }, renameFailed: { title: "Umbenennen fehlgeschlagen" }, + deleteSuccess: { title: "Arbeitsbereich gelöscht" }, + deleteFailed: { title: "Löschen fehlgeschlagen" }, }, } as const;