Files
webkulisse-saas-tanstack/aktuelle TODO.md
T

300 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### Security- und Plausibilitäts-Review: Kundenportal & Authentifizierung
Hier ist das detaillierte Sicherheits-, Architektur- und Plausibilitäts-Review für das Kundenportal von **webkulisse**. Das Review analysiert den Ist-Zustand (Authentifizierung auf Basis von Better-Auth und TanStack Start / React Router) und liefert konkrete Handlungsempfehlungen, damit künftige Module (**Rechnungsverwaltung** und **Produktabnahmen**) modular, mandantensicher und ohne Spaghetti-Code integriert werden können.
---
### 1. Priorisierte Findings & Sicherheitsrisiken
---
#### 🔴 KRITISCH: Fehlendes Mandanten- & Autorisierungskonzept für Multi-Kunden-Daten (IDOR-Gefahr)
* **Dateiverweis:** `src/db/schema.ts`, `src/lib/auth.ts`, `src/lib/session.ts`
* **Problem & Risiko:**
Das aktuelle Schema speichert lediglich flache `user`-Einträge mit einer einfachen Rolle (`role: 'user' | 'admin'`). Es gibt keine Mandanten- bzw. Kundenentität (`Organization`, `Customer` oder `Tenant`).
Wenn Rechnungen und Abnahmen direkt auf `userId` referenzieren:
1. Kann ein Kunde nicht mehrere Mitarbeiter/Ansprechpartner haben (typischer B2B-Anwendungsfall: Inhaber + Buchhaltung).
2. Besteht ohne standardisierte Mandanten-Middleware bei kommenden Server-Funktionen ein extremes Risiko für **IDOR (Insecure Direct Object Reference)** z. B. wenn ein Kunde über manipulierte IDs Rechnungen oder Abnahmen anderer Kunden abrufen oder freigeben kann.
* **Lösungsempfehlung:**
Einführung einer expliziten `customer`/`organization`-Tabelle und einer typsicheren Server-Function-Middleware, die bei jedem Datenzugriff den aktuellen Mandantenkontext prüft.
```ts
// src/db/schema.ts - Empfohlenes Schema-Fundament
export const customer = pgTable('customer', {
id: text('id').primaryKey(),
name: text('name').notNull(), // Firmenname z. B. 'Praxis Dr. Schmidt'
customerNumber: text('customer_number').notNull().unique(), // z. B. KD-2026-001
status: text('status').notNull().default('active'),
createdAt: timestamp('created_at').notNull().defaultNow(),
})
export const user = pgTable('user', {
// ... bestehende Better-Auth Felder
customerId: text('customer_id').references(() => customer.id, { onDelete: 'cascade' }),
})
```
---
#### 🔴 KRITISCH: Devtools und sensible Debugger-Panels im Produktions-Bundle
* **Dateiverweis:** `src/routes/__root.tsx` (Zeilen 94104), `vite.config.ts` (Zeile 14)
* **Problem & Risiko:**
`<TanStackDevtools />` und `<TanStackRouterDevtoolsPanel />` werden im `RootDocument` bedingungslos gerendert. Dadurch werden Router-Zustände, Context-Daten und interne Routing-Strukturen im Browser für jeden Besucher sichtbar.
* **Lösungsempfehlung:**
Devtools nur in der Entwicklungsumgebung laden und im Build strippen:
```tsx
// src/routes/__root.tsx
{process.env.NODE_ENV !== 'production' ? (
<TanStackDevtools
config={{ position: 'bottom-right' }}
plugins={[{ name: 'Tanstack Router', render: <TanStackRouterDevtoolsPanel /> }]}
/>
) : null}
```
---
#### 🟠 HOCH: Kein Self-Service-Passwort-Reset / Unsichere initiale Passwort-Weitergabe
* **Dateiverweis:** `src/routes/_admin/admin.users.new.tsx` (Zeilen 86101), `src/routes/login.tsx`
* **Problem & Risiko:**
1. Beim Erstellen eines Kunden (`admin.users.new.tsx`) muss der Admin ein Klartext-Passwort vergeben, welches als normales Input-Feld (`type="text"`) sichtbar ist.
2. Admins müssen dieses Initialpasswort unverschlüsselt (per Mail/Chat) an Kunden weitergeben.
3. Es gibt im Kunden-Login keinen "Passwort vergessen?"-Flow (`/reset-password`).
4. Kunden haben keine Möglichkeit, ihr Passwort nach dem Erst-Login selbstständig zu ändern.
* **Lösungsempfehlung:**
1. **Einladungs-Link / Magic-Link Flow:** Statt manueller Passwörter generiert der Admin einen zeitlich befristeten Aktivierungslink via Better-Auth Invitation/Reset-Token.
2. Implementierung eines standardisierten `Passwort vergessen`-Flows im `login.tsx`.
---
#### 🟠 HOCH: In-Memory Rate Limiting bei verteilten Deployments
* **Dateiverweis:** `src/lib/auth.ts` (Zeilen 3950)
* **Problem & Risiko:**
Das Rate Limiting ist auf `storage: 'memory'` konfiguriert:
```ts
rateLimit: {
enabled: true,
storage: 'memory',
customRules: {
'/sign-in/email': { window: 60, max: 5 },
// ...
}
}
```
In Serverless-, Container- oder Node-Cluster-Umgebungen (z. B. Nitro / Cloudflare / Node Multi-Worker) teilt sich jeder Worker einen eigenen Speicher. Bei Server-Neustarts oder mehreren Instanzen ist der Brute-Force-Schutz wirkungslos oder leicht zu umgehen.
* **Lösungsempfehlung:**
Anbindung eines persistenten Speichers (z. B. Redis / Upstash / Postgres-basiertes Rate Limiting) oder Absicherung über Cloudflare WAF / Reverse Proxy.
---
#### 🟡 MITTEL: Client-seitiges Data-Fetching mit Wasserfällen im Admin- & Kundenbereich
* **Dateiverweis:** `src/routes/_admin/admin.index.tsx` (Zeilen 2540), `src/routes/_admin/admin.users.$userId.tsx` (Zeilen 3562)
* **Problem & Architektur-Defizit:**
Daten werden per `useEffect` und lokalem `useState` clientseitig nachgeladen ("Lade Kunden ...").
* Dies widerspricht dem Konzept von TanStack Start / Router (Loader-first SSR).
* Führt zu Layout-Shifts, fehlenden SSR-Vorteilen und erschwert Caching, Revalidierung und granulare Fehlergrenzen (`CatchBoundary`).
* **Lösungsempfehlung:**
Nutzung von `loader` in Kombination mit Server Functions (`createServerFn`):
```tsx
// src/routes/_admin/admin.index.tsx
export const Route = createFileRoute('/_admin/admin/')({
loader: async () => await listUsersFn(),
component: AdminUsersPage,
})
function AdminUsersPage() {
const users = Route.useLoaderData()
// Kein useEffect / manual loading state nötig
}
```
---
#### 🟡 MITTEL: Fehlende Sicherheits-Header (CSP, HSTS, X-Frame-Options)
* **Dateiverweis:** `vite.config.ts`, `src/routes/__root.tsx`
* **Problem & Risiko:**
Es sind keine HTTP-Security-Header konfiguriert. Im `__root.tsx` wird ein Inline-Script (`themeItnitScript`) ohne Nonce ausgeführt.
* **Lösungsempfehlung:**
In Nitro/Vite oder im Webserver (Nginx/Cloudflare) Header setzen:
* `Content-Security-Policy` (CSP)
* `X-Frame-Options: DENY` (Schutz vor Clickjacking)
* `X-Content-Type-Options: nosniff`
* `Referrer-Policy: strict-origin-when-cross-origin`
* `Strict-Transport-Security: max-age=31536000; includeSubDomains`
---
#### 🟢 NIEDRIG: Fehlende Domain-Zentralisierung & Redundante Rollenprüfungen
* **Dateiverweis:** `src/components/header.tsx` (Zeilen 2226), `src/routes/login.tsx` (Zeilen 10, 47), `src/components/app-shell.tsx` (Zeile 58)
* **Problem:**
Die Weiterleitungs- und Rollenlogik `user.role === 'admin' ? '/admin' : '/dashboard'` ist redundant über mehrere Komponenten verstreut.
* **Lösungsempfehlung:**
Zentralisierung in einem Auth-Helper, z. B. `getDefaultRedirect(session)` in `src/lib/auth-utils.ts`.
---
### 2. Konkreter Architektur-Blueprint für Rechnungen & Abnahmen
Um **Spaghetti-Code** und spätere Umbauten an der Auth-Basis zu vermeiden, sollte die Codebasis jetzt modular strukturiert werden.
#### A. Empfohlene Feature-basierte Verzeichnisstruktur
```text
src/
├── db/
│ ├── schema/
│ │ ├── auth.ts # user, session, account, verification
│ │ ├── customers.ts # customer / tenant Stammdaten
│ │ ├── projects.ts # Webseiten-Projekte
│ │ ├── invoices.ts # Rechnungen, Posten, Zahlungsstatus
│ │ └── approvals.ts # Produktabnahmen, Feedback-Runden
│ └── index.ts # Drizzle DB Instance & Schema-Export
├── server/
│ ├── middleware/
│ │ ├── auth.ts # Authentifizierungs-Guard
│ │ └── tenant.ts # Mandanten-Isolation (Tenant Context)
│ └── services/ # Pure Business Logic (unabhängig von UI)
│ ├── invoice.service.ts
│ └── approval.service.ts
├── routes/
│ ├── _authenticated/
│ │ ├── route.tsx # Auth Guard & Portal Layout
│ │ ├── dashboard.tsx # Kunden-Übersicht
│ │ ├── rechnungen/ # Rechnungsmodul (Kunde)
│ │ │ ├── index.tsx
│ │ │ └── $invoiceId.tsx
│ │ └── abnahmen/ # Produktabnahmen (Kunde)
│ │ ├── index.tsx
│ │ └── $approvalId.tsx
│ └── _admin/
│ ├── admin.tsx # Admin Layout
│ ├── rechnungen/ # Rechnungsverwaltung (Admin)
│ └── kunden/ # Kundenverwaltung
```
---
#### B. Datenmodell-Erweiterung (`src/db/schema/`)
```ts
// 1. Projekte (Klammer um Rechnungen und Abnahmen)
export const project = pgTable('project', {
id: text('id').primaryKey(),
customerId: text('customer_id').notNull().references(() => customer.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
status: text('status').notNull().default('in_progress'), // 'in_progress' | 'review' | 'live'
stagingUrl: text('staging_url'),
createdAt: timestamp('created_at').notNull().defaultNow(),
})
// 2. Rechnungsmodul
export const invoice = pgTable('invoice', {
id: text('id').primaryKey(),
customerId: text('customer_id').notNull().references(() => customer.id, { onDelete: 'cascade' }),
projectId: text('project_id').references(() => project.id, { onDelete: 'set null' }),
invoiceNumber: text('invoice_number').notNull().unique(), // z. B. RE-2026-0042
amountGrossInCents: integer('amount_gross_in_cents').notNull(),
taxRate: numeric('tax_rate').notNull().default('19.00'),
status: text('status').notNull().default('open'), // 'open' | 'paid' | 'overdue' | 'cancelled'
pdfStoragePath: text('pdf_storage_path').notNull(),
dueDate: timestamp('due_date').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
})
// 3. Produktabnahme-Modul
export const approval = pgTable('approval', {
id: text('id').primaryKey(),
projectId: text('project_id').notNull().references(() => project.id, { onDelete: 'cascade' }),
customerId: text('customer_id').notNull().references(() => customer.id, { onDelete: 'cascade' }),
version: text('version').notNull(), // z. B. 'v1.0 - Design-Entwurf'
status: text('status').notNull().default('pending'), // 'pending' | 'approved' | 'changes_requested'
feedback: text('feedback'),
approvedAt: timestamp('approved_at'),
approvedByUserId: text('approved_by_user_id').references(() => user.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
})
```
---
#### C. Sichere Server-Function-Middleware (Mandanten-Isolation)
Mit TanStack Start Middleware wird sichergestellt, dass jede Abfrage automatisch auf den Mandanten des eingeloggten Benutzers eingeschränkt ist:
```ts
// src/server/middleware/tenant.ts
import { createMiddleware } from '@tanstack/react-start'
import { getRequest } from '@tanstack/react-start/server'
import { auth } from '#/lib/auth.ts'
export const tenantAuthMiddleware = createMiddleware().server(async ({ next }) => {
const request = getRequest()
const session = await auth.api.getSession({ headers: request.headers })
if (!session || !session.user) {
throw new Error('UNAUTHORIZED')
}
return next({
context: {
session,
userId: session.user.id,
role: session.user.role,
// Bei Kunden wird die customerId erzwungen; Admins dürfen filtern
customerId: (session.user as any).customerId as string | null,
isAdmin: session.user.role === 'admin',
},
})
})
```
**Verwendung in Server Functions (IDOR-sicher):**
```ts
// src/server/functions/invoices.ts
import { createServerFn } from '@tanstack/react-start'
import { tenantAuthMiddleware } from '../middleware/tenant'
import { db } from '#/db/index.ts'
import { invoice } from '#/db/schema.ts'
import { eq, and } from 'drizzle-orm'
export const getInvoicesForCurrentCustomer = createServerFn({ method: 'GET' })
.middleware([tenantAuthMiddleware])
.handler(async ({ context }) => {
if (!context.customerId && !context.isAdmin) {
return []
}
// Automatisch auf den Mandanten des eingeloggten Nutzers begrenzt!
return await db.query.invoice.findMany({
where: context.isAdmin
? undefined
: eq(invoice.customerId, context.customerId!),
orderBy: (inv, { desc }) => [desc(inv.createdAt)],
})
})
```
---
### 3. Zusammenfassung des Handlungsbedarfs
| Priorität | Bereich | Maßnahme |
| :--- | :--- | :--- |
| **Kritisch** | Architektur | `Customer`/`Tenant`-Entität im DB-Schema ergänzen & `tenantAuthMiddleware` etablieren. |
| **Kritisch** | Sicherheit | Devtools in `__root.tsx` hinter `process.env.NODE_ENV !== 'production'` kapseln. |
| **Hoch** | Auth / UX | Passwort-Reset-Flow & Einladungs-Link für Kunden einrichten (Klartext-Passwortvergabe im Admin ablösen). |
| **Mittel** | Performance / Code | Admin- und Kunden-Views von `useEffect`-Fetching auf TanStack Route Loader umstellen. |
| **Mittel** | Sicherheit | HTTP-Sicherheitsheader (CSP, HSTS, X-Frame-Options) konfigurieren. |
Mit dieser Vorbereitung ist die Basis optimal gehärtet und Rechnungs- sowie Abnahme-Features können sauber als eigenständige Domain-Module angedockt werden.