Initial commit from template
This commit is contained in:
286
.claude/commands/api.md
Normal file
286
.claude/commands/api.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# API Route Generator
|
||||
|
||||
Du bist ein Experte für Next.js API Routes. Erstelle sichere, typisierte API Endpoints.
|
||||
|
||||
## Deine Aufgaben
|
||||
|
||||
Erstelle API Routes die:
|
||||
- Next.js App Router Patterns folgen
|
||||
- TypeScript mit korrekten Types verwenden
|
||||
- Error Handling implementieren
|
||||
- Input Validation durchführen
|
||||
- Supabase Integration nutzen
|
||||
|
||||
## API Route Template
|
||||
|
||||
```typescript
|
||||
// app/api/[resource]/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||||
);
|
||||
|
||||
// GET - Liste oder einzelnes Item
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (id) {
|
||||
// Single item
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
// List
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('GET error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch data' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Neues Item erstellen
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!body.name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Name is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.insert(body)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return NextResponse.json(data, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('POST error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create item' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Item aktualisieren
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { id, ...updates } = body;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'ID is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.update(updates)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('PUT error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update item' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Item löschen
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'ID is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('table')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('DELETE error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete item' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Route
|
||||
|
||||
```typescript
|
||||
// app/api/users/[id]/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
const { id } = await params;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Auth Protected Route
|
||||
|
||||
```typescript
|
||||
// app/api/protected/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createServerClient } from '@supabase/ssr';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value;
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
|
||||
if (error || !user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// User is authenticated
|
||||
return NextResponse.json({ user });
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation mit Zod
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(2).max(100),
|
||||
role: z.enum(['user', 'admin']).optional().default('user'),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input
|
||||
const result = createUserSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: result.error.flatten() },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const validatedData = result.data;
|
||||
// ... create user
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Response Helpers
|
||||
|
||||
```typescript
|
||||
// lib/api-response.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export function successResponse<T>(data: T, status = 200) {
|
||||
return NextResponse.json({ success: true, data }, { status });
|
||||
}
|
||||
|
||||
export function errorResponse(message: string, status = 500) {
|
||||
return NextResponse.json({ success: false, error: message }, { status });
|
||||
}
|
||||
|
||||
export function notFoundResponse(resource = 'Resource') {
|
||||
return errorResponse(`${resource} not found`, 404);
|
||||
}
|
||||
|
||||
export function unauthorizedResponse() {
|
||||
return errorResponse('Unauthorized', 401);
|
||||
}
|
||||
|
||||
export function validationErrorResponse(errors: unknown) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Validation failed', details: errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Error Handling** - Immer try/catch verwenden
|
||||
2. **Input Validation** - Alle Inputs validieren (Zod empfohlen)
|
||||
3. **Auth Check** - Geschützte Routen absichern
|
||||
4. **Status Codes** - Korrekte HTTP Status Codes
|
||||
5. **Logging** - Errors loggen für Debugging
|
||||
|
||||
---
|
||||
|
||||
Frage den Benutzer: Welche API Route möchtest du erstellen?
|
||||
Beschreibe die gewünschte Ressource und Operationen.
|
||||
249
.claude/commands/component.md
Normal file
249
.claude/commands/component.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# UI Component Generator
|
||||
|
||||
Du bist ein Experte für React/Next.js Components mit Tailwind CSS und Spartan UI. Erstelle moderne, wiederverwendbare UI Components.
|
||||
|
||||
## Deine Aufgaben
|
||||
|
||||
Erstelle Components die:
|
||||
- TypeScript mit korrekten Props-Typen verwenden
|
||||
- Tailwind CSS für Styling nutzen
|
||||
- Spartan UI Patterns folgen
|
||||
- Accessible sind (ARIA, Keyboard Navigation)
|
||||
- Server/Client Component korrekt trennen
|
||||
|
||||
## Component Template
|
||||
|
||||
```typescript
|
||||
// components/ui/[ComponentName].tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ComponentNameProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
variant?: 'default' | 'primary' | 'secondary';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function ComponentName({
|
||||
children,
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
}: ComponentNameProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
// Base styles
|
||||
'rounded-lg border',
|
||||
// Variants
|
||||
{
|
||||
'bg-background': variant === 'default',
|
||||
'bg-primary text-primary-foreground': variant === 'primary',
|
||||
'bg-secondary text-secondary-foreground': variant === 'secondary',
|
||||
},
|
||||
// Sizes
|
||||
{
|
||||
'p-2 text-sm': size === 'sm',
|
||||
'p-4 text-base': size === 'md',
|
||||
'p-6 text-lg': size === 'lg',
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Häufige Components
|
||||
|
||||
### Button
|
||||
```typescript
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'default' | 'primary' | 'outline' | 'ghost' | 'destructive';
|
||||
size?: 'sm' | 'md' | 'lg' | 'icon';
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
disabled,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'primary',
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'default',
|
||||
'border border-input hover:bg-accent': variant === 'outline',
|
||||
'hover:bg-accent': variant === 'ghost',
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
|
||||
},
|
||||
{
|
||||
'h-8 px-3 text-sm': size === 'sm',
|
||||
'h-10 px-4': size === 'md',
|
||||
'h-12 px-6 text-lg': size === 'lg',
|
||||
'h-10 w-10': size === 'icon',
|
||||
},
|
||||
className
|
||||
)}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
>
|
||||
{loading && <Spinner className="mr-2 h-4 w-4" />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Card
|
||||
```typescript
|
||||
export function Card({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={cn('rounded-xl border bg-card p-6 shadow-sm', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardHeader({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <div className={cn('mb-4', className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function CardTitle({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <h3 className={cn('text-xl font-semibold', className)}>{children}</h3>;
|
||||
}
|
||||
|
||||
export function CardContent({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <div className={cn('', className)}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Input
|
||||
```typescript
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function Input({ label, error, className, id, ...props }: InputProps) {
|
||||
const inputId = id || label?.toLowerCase().replace(/\s/g, '-');
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="text-sm font-medium">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={inputId}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-sm placeholder:text-muted-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
error && 'border-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Modal/Dialog
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Modal({ open, onClose, children, className }: ModalProps) {
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-50 w-full max-w-lg rounded-lg bg-background p-6 shadow-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Client vs Server Components
|
||||
|
||||
```typescript
|
||||
// Server Component (default) - keine Interaktivität
|
||||
// components/UserList.tsx
|
||||
export async function UserList() {
|
||||
const users = await fetchUsers();
|
||||
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
||||
}
|
||||
|
||||
// Client Component - mit Interaktivität
|
||||
// components/Counter.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **cn() Helper** - Für conditional classes
|
||||
2. **Forwardref** - Für DOM-Zugriff von außen
|
||||
3. **Composition** - Kleine, kombinierbare Components
|
||||
4. **Accessibility** - ARIA Labels, Keyboard Support
|
||||
5. **Dark Mode** - CSS Variables für Theming
|
||||
|
||||
---
|
||||
|
||||
Frage den Benutzer: Welche Component möchtest du erstellen?
|
||||
Beschreibe die gewünschte Funktionalität und das Design.
|
||||
157
.claude/commands/db-connect.md
Normal file
157
.claude/commands/db-connect.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# PostgreSQL Datenbank Verbindung
|
||||
|
||||
Du verbindest dich mit der PostgreSQL Datenbank des Projekts.
|
||||
|
||||
## Automatischer Setup
|
||||
|
||||
### 1. Betriebssystem erkennen
|
||||
|
||||
Prüfe zuerst das Betriebssystem:
|
||||
|
||||
```bash
|
||||
uname -s
|
||||
```
|
||||
|
||||
### 2. PostgreSQL CLI installieren (falls nicht vorhanden)
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
# Prüfen ob psql installiert ist
|
||||
which psql || brew install postgresql
|
||||
```
|
||||
|
||||
**Linux (Ubuntu/Debian):**
|
||||
```bash
|
||||
which psql || sudo apt-get update && sudo apt-get install -y postgresql-client
|
||||
```
|
||||
|
||||
**Linux (Alpine):**
|
||||
```bash
|
||||
which psql || apk add postgresql-client
|
||||
```
|
||||
|
||||
### 3. Environment Variable lesen
|
||||
|
||||
Lese die `DATABASE_URL` aus `.env.local` oder `.env`:
|
||||
|
||||
```bash
|
||||
# Aus .env.local
|
||||
grep DATABASE_URL .env.local 2>/dev/null || grep DATABASE_URL .env 2>/dev/null
|
||||
```
|
||||
|
||||
Die URL hat das Format:
|
||||
```
|
||||
postgresql://USER:PASSWORD@HOST:PORT/DATABASE
|
||||
```
|
||||
|
||||
### 4. Mit Datenbank verbinden
|
||||
|
||||
```bash
|
||||
# Direkte Verbindung mit URL
|
||||
psql "$DATABASE_URL"
|
||||
|
||||
# Oder mit Supabase
|
||||
psql "postgresql://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres"
|
||||
```
|
||||
|
||||
## Häufige Befehle nach Verbindung
|
||||
|
||||
```sql
|
||||
-- Alle Tabellen anzeigen
|
||||
\dt
|
||||
|
||||
-- Tabellen mit Schema
|
||||
\dt public.*
|
||||
|
||||
-- Tabellen-Details
|
||||
\d table_name
|
||||
|
||||
-- Alle Schemas
|
||||
\dn
|
||||
|
||||
-- Benutzer anzeigen
|
||||
\du
|
||||
|
||||
-- Aktuelle Datenbank
|
||||
SELECT current_database();
|
||||
|
||||
-- Tabellen-Größen
|
||||
SELECT
|
||||
tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) as size
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;
|
||||
```
|
||||
|
||||
## Supabase spezifische Queries
|
||||
|
||||
```sql
|
||||
-- Auth Users (Supabase)
|
||||
SELECT id, email, created_at FROM auth.users LIMIT 10;
|
||||
|
||||
-- Storage Buckets
|
||||
SELECT * FROM storage.buckets;
|
||||
|
||||
-- RLS Policies
|
||||
SELECT * FROM pg_policies WHERE schemaname = 'public';
|
||||
|
||||
-- Enabled Extensions
|
||||
SELECT * FROM pg_extension;
|
||||
```
|
||||
|
||||
## SQL Datei ausführen
|
||||
|
||||
```bash
|
||||
# SQL Datei ausführen
|
||||
psql "$DATABASE_URL" -f migrations/001_init.sql
|
||||
|
||||
# Mit Output
|
||||
psql "$DATABASE_URL" -f migrations/001_init.sql -v ON_ERROR_STOP=1
|
||||
```
|
||||
|
||||
## Backup & Restore
|
||||
|
||||
```bash
|
||||
# Backup erstellen
|
||||
pg_dump "$DATABASE_URL" > backup.sql
|
||||
|
||||
# Nur Schema
|
||||
pg_dump "$DATABASE_URL" --schema-only > schema.sql
|
||||
|
||||
# Nur Daten
|
||||
pg_dump "$DATABASE_URL" --data-only > data.sql
|
||||
|
||||
# Restore
|
||||
psql "$DATABASE_URL" < backup.sql
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection refused
|
||||
- Supabase Projekt ist pausiert → Dashboard öffnen
|
||||
- Firewall blockiert Port 5432/6543
|
||||
- IP nicht in Allowlist (Supabase: Database Settings > Connection Pooling)
|
||||
|
||||
### SSL required
|
||||
```bash
|
||||
psql "$DATABASE_URL?sslmode=require"
|
||||
```
|
||||
|
||||
### Password authentication failed
|
||||
- Passwort in URL URL-encoded? (`@` → `%40`, etc.)
|
||||
- Richtiges Passwort aus Supabase Dashboard
|
||||
|
||||
---
|
||||
|
||||
## Automatische Verbindung
|
||||
|
||||
Führe diese Schritte aus:
|
||||
|
||||
1. **OS prüfen:** `uname -s`
|
||||
2. **psql prüfen:** `which psql`
|
||||
3. **Falls nicht vorhanden:** Installiere je nach OS
|
||||
4. **DATABASE_URL laden:** Aus .env.local oder .env
|
||||
5. **Verbinden:** `psql "$DATABASE_URL"`
|
||||
|
||||
Soll ich die Verbindung jetzt herstellen?
|
||||
220
.claude/commands/deploy.md
Normal file
220
.claude/commands/deploy.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Deployment Assistent
|
||||
|
||||
Du bist ein Experte für das Deployment von Next.js Anwendungen mit Docker und Kubernetes.
|
||||
|
||||
## Deployment Optionen
|
||||
|
||||
### 1. Lokales Development
|
||||
|
||||
```bash
|
||||
# Dependencies installieren
|
||||
pnpm install
|
||||
|
||||
# Environment Variables
|
||||
cp .env.example .env.local
|
||||
# .env.local bearbeiten
|
||||
|
||||
# Development Server starten
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 2. Docker Deployment
|
||||
|
||||
#### Docker Image bauen
|
||||
|
||||
```bash
|
||||
# Production Image bauen
|
||||
docker build -t lumina-app:latest .
|
||||
|
||||
# Mit spezifischem Tag
|
||||
docker build -t lumina-app:v1.0.0 .
|
||||
|
||||
# Lokaler Test
|
||||
docker run -p 3000:3000 --env-file .env.local lumina-app:latest
|
||||
```
|
||||
|
||||
#### Docker Compose (empfohlen für lokales Testing)
|
||||
|
||||
```bash
|
||||
# Alle Services starten
|
||||
docker-compose up -d
|
||||
|
||||
# Logs ansehen
|
||||
docker-compose logs -f app
|
||||
|
||||
# Services stoppen
|
||||
docker-compose down
|
||||
|
||||
# Mit Volume Cleanup
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### 3. Harbor Registry
|
||||
|
||||
```bash
|
||||
# Bei Harbor einloggen
|
||||
docker login harbor.advisori.de
|
||||
|
||||
# Image taggen
|
||||
docker tag lumina-app:latest harbor.advisori.de/lumina/PROJECT_ID:latest
|
||||
docker tag lumina-app:latest harbor.advisori.de/lumina/PROJECT_ID:v1.0.0
|
||||
|
||||
# Pushen
|
||||
docker push harbor.advisori.de/lumina/PROJECT_ID:latest
|
||||
docker push harbor.advisori.de/lumina/PROJECT_ID:v1.0.0
|
||||
```
|
||||
|
||||
### 4. Kubernetes Deployment
|
||||
|
||||
#### Voraussetzungen
|
||||
|
||||
```bash
|
||||
# Namespace erstellen
|
||||
kubectl create namespace lumina-apps
|
||||
|
||||
# Harbor Pull Secret
|
||||
kubectl create secret docker-registry harbor-registry-secret \
|
||||
--docker-server=harbor.advisori.de \
|
||||
--docker-username=YOUR_USERNAME \
|
||||
--docker-password=YOUR_PASSWORD \
|
||||
--namespace=lumina-apps
|
||||
|
||||
# Application Secrets
|
||||
kubectl create secret generic app-secrets \
|
||||
--from-literal=DATABASE_URL="postgresql://..." \
|
||||
--from-literal=SUPABASE_URL="https://..." \
|
||||
--from-literal=SUPABASE_ANON_KEY="..." \
|
||||
--from-literal=SUPABASE_SERVICE_ROLE_KEY="..." \
|
||||
--namespace=lumina-apps
|
||||
```
|
||||
|
||||
#### Helm Deployment
|
||||
|
||||
```bash
|
||||
# In das Helm Chart Verzeichnis wechseln
|
||||
cd helm/lumina-app
|
||||
|
||||
# Dry-run zum Testen
|
||||
helm upgrade --install app . \
|
||||
--namespace lumina-apps \
|
||||
--set image.repository=harbor.advisori.de/lumina/PROJECT_ID \
|
||||
--set image.tag=latest \
|
||||
--dry-run
|
||||
|
||||
# Tatsächliches Deployment
|
||||
helm upgrade --install app . \
|
||||
--namespace lumina-apps \
|
||||
--set image.repository=harbor.advisori.de/lumina/PROJECT_ID \
|
||||
--set image.tag=v1.0.0 \
|
||||
--set ingress.hosts[0].host=app.advisori.de \
|
||||
--set ingress.tls[0].hosts[0]=app.advisori.de
|
||||
|
||||
# Status prüfen
|
||||
helm status app -n lumina-apps
|
||||
```
|
||||
|
||||
#### Kubectl Management
|
||||
|
||||
```bash
|
||||
# Pods anzeigen
|
||||
kubectl get pods -n lumina-apps
|
||||
|
||||
# Logs ansehen
|
||||
kubectl logs -f deployment/app -n lumina-apps
|
||||
|
||||
# In Pod exec
|
||||
kubectl exec -it deployment/app -n lumina-apps -- /bin/sh
|
||||
|
||||
# Restart Deployment
|
||||
kubectl rollout restart deployment/app -n lumina-apps
|
||||
|
||||
# Rollback
|
||||
helm rollback app -n lumina-apps
|
||||
```
|
||||
|
||||
### 5. CI/CD Pipeline (Gitea Actions)
|
||||
|
||||
```yaml
|
||||
# .gitea/workflows/deploy.yml
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: harbor.advisori.de
|
||||
username: ${{ secrets.HARBOR_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_PASSWORD }}
|
||||
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
harbor.advisori.de/lumina/${{ github.event.repository.name }}:latest
|
||||
harbor.advisori.de/lumina/${{ github.event.repository.name }}:${{ github.sha }}
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to K8s
|
||||
uses: azure/k8s-deploy@v4
|
||||
with:
|
||||
namespace: lumina-apps
|
||||
manifests: helm/lumina-app
|
||||
images: harbor.advisori.de/lumina/${{ github.event.repository.name }}:${{ github.sha }}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container startet nicht
|
||||
```bash
|
||||
# Pod Status
|
||||
kubectl describe pod POD_NAME -n lumina-apps
|
||||
|
||||
# Container Logs
|
||||
kubectl logs POD_NAME -n lumina-apps --previous
|
||||
```
|
||||
|
||||
### Ingress funktioniert nicht
|
||||
```bash
|
||||
# Ingress Status
|
||||
kubectl get ingress -n lumina-apps
|
||||
|
||||
# Ingress Details
|
||||
kubectl describe ingress app -n lumina-apps
|
||||
```
|
||||
|
||||
### Health Check Fehler
|
||||
```bash
|
||||
# Health Endpoint testen
|
||||
kubectl port-forward svc/app 3000:80 -n lumina-apps
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
## Environment Variables Checkliste
|
||||
|
||||
| Variable | Beschreibung | Erforderlich |
|
||||
|----------|--------------|--------------|
|
||||
| `NODE_ENV` | production | Ja |
|
||||
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase URL | Ja |
|
||||
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Anon Key | Ja |
|
||||
| `SUPABASE_SERVICE_ROLE_KEY` | Service Key | Ja |
|
||||
| `DATABASE_URL` | PostgreSQL URL | Optional |
|
||||
|
||||
---
|
||||
|
||||
Frage den Benutzer: Wie möchtest du deployen?
|
||||
- Lokal mit Docker Compose
|
||||
- Auf Kubernetes Cluster
|
||||
- CI/CD Pipeline einrichten
|
||||
176
.claude/commands/setup.md
Normal file
176
.claude/commands/setup.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Projekt Setup Assistent
|
||||
|
||||
Du hilfst beim initialen Setup eines neuen Lumina-Projekts.
|
||||
|
||||
## Setup Checkliste
|
||||
|
||||
### 1. Dependencies installieren
|
||||
|
||||
```bash
|
||||
# Mit pnpm (empfohlen)
|
||||
pnpm install
|
||||
|
||||
# Oder mit npm
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Environment Variables konfigurieren
|
||||
|
||||
```bash
|
||||
# Beispiel-Datei kopieren
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
Bearbeite `.env.local`:
|
||||
|
||||
```env
|
||||
# Supabase (erforderlich)
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
|
||||
|
||||
# Optional
|
||||
DATABASE_URL=postgresql://user:pass@host:5432/db
|
||||
REDIS_URL=redis://localhost:6379
|
||||
```
|
||||
|
||||
### 3. Supabase Projekt erstellen
|
||||
|
||||
1. Gehe zu [supabase.com](https://supabase.com)
|
||||
2. Erstelle ein neues Projekt
|
||||
3. Kopiere URL und Keys aus Project Settings > API
|
||||
4. Füge sie in `.env.local` ein
|
||||
|
||||
### 4. Datenbank initialisieren
|
||||
|
||||
Falls du SQL Migrationen hast:
|
||||
|
||||
```bash
|
||||
# Via Supabase CLI
|
||||
npx supabase db push
|
||||
|
||||
# Oder via Dashboard
|
||||
# - Gehe zu SQL Editor
|
||||
# - Führe deine Migrations aus
|
||||
```
|
||||
|
||||
### 5. Development Server starten
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Öffne [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## Projekt Struktur anlegen
|
||||
|
||||
### Empfohlene Ordner
|
||||
|
||||
```bash
|
||||
# UI Components
|
||||
mkdir -p components/ui
|
||||
mkdir -p components/forms
|
||||
mkdir -p components/layout
|
||||
|
||||
# Features
|
||||
mkdir -p components/features
|
||||
|
||||
# API Helpers
|
||||
mkdir -p lib/api
|
||||
mkdir -p lib/hooks
|
||||
|
||||
# Types
|
||||
mkdir -p types
|
||||
```
|
||||
|
||||
### Basis-Files erstellen
|
||||
|
||||
```typescript
|
||||
// types/index.ts
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
avatar_url?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// lib/hooks/useUser.ts
|
||||
'use client';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
export function useUser() {
|
||||
const { user, loading } = useAuth();
|
||||
return { user, loading };
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Types aus Supabase generieren
|
||||
|
||||
```bash
|
||||
# Supabase CLI installieren
|
||||
pnpm add -D supabase
|
||||
|
||||
# Login
|
||||
npx supabase login
|
||||
|
||||
# Types generieren
|
||||
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > lib/database.types.ts
|
||||
```
|
||||
|
||||
Verwendung:
|
||||
|
||||
```typescript
|
||||
import { Database } from '@/lib/database.types';
|
||||
|
||||
type User = Database['public']['Tables']['users']['Row'];
|
||||
type NewUser = Database['public']['Tables']['users']['Insert'];
|
||||
```
|
||||
|
||||
## Git Setup
|
||||
|
||||
```bash
|
||||
# Falls noch nicht initialisiert
|
||||
git init
|
||||
|
||||
# Initial Commit
|
||||
git add .
|
||||
git commit -m "Initial setup"
|
||||
|
||||
# Remote hinzufügen (Gitea)
|
||||
git remote add origin https://gitea.example.com/org/repo.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
Nach dem Setup kannst du:
|
||||
|
||||
1. **Auth einrichten** - `/supabase-auth`
|
||||
2. **Datenbank Schema erstellen** - `/supabase-db`
|
||||
3. **UI Components bauen** - `/component`
|
||||
4. **API Routes erstellen** - `/api`
|
||||
|
||||
## Häufige Probleme
|
||||
|
||||
### "Module not found"
|
||||
```bash
|
||||
# Cache löschen
|
||||
rm -rf .next node_modules
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Supabase Connection Error
|
||||
- URL und Keys prüfen
|
||||
- Supabase Projekt ist aktiv (nicht pausiert)
|
||||
- Anon Key hat Lese-Rechte
|
||||
|
||||
### TypeScript Errors
|
||||
```bash
|
||||
# Type Check
|
||||
pnpm run lint
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Frage den Benutzer: Was möchtest du als nächstes einrichten?
|
||||
287
.claude/commands/supabase-auth.md
Normal file
287
.claude/commands/supabase-auth.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Supabase Authentication Assistent
|
||||
|
||||
Du bist ein Experte für Supabase Authentication. Hilf dem Benutzer bei Auth-Setup, User Management und Session Handling.
|
||||
|
||||
## Deine Aufgaben
|
||||
|
||||
### 1. Auth Setup
|
||||
|
||||
```typescript
|
||||
// lib/supabase-auth.ts
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
// Browser Client
|
||||
export const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
// Server Client (für API Routes)
|
||||
export const supabaseAdmin = createClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Sign Up / Sign In
|
||||
|
||||
```typescript
|
||||
// Email/Password Sign Up
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email: 'user@example.com',
|
||||
password: 'secure-password',
|
||||
options: {
|
||||
data: {
|
||||
full_name: 'John Doe',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Email/Password Sign In
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email: 'user@example.com',
|
||||
password: 'password'
|
||||
});
|
||||
|
||||
// Magic Link
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email: 'user@example.com',
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/auth/callback`
|
||||
}
|
||||
});
|
||||
|
||||
// OAuth (Google, GitHub, etc.)
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Session Management
|
||||
|
||||
```typescript
|
||||
// Get current session
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
// Get current user
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
// Listen to auth changes
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event === 'SIGNED_IN') {
|
||||
console.log('User signed in:', session?.user);
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
console.log('User signed out');
|
||||
}
|
||||
});
|
||||
|
||||
// Sign out
|
||||
await supabase.auth.signOut();
|
||||
```
|
||||
|
||||
### 4. Auth Context Provider
|
||||
|
||||
```typescript
|
||||
// contexts/AuthContext.tsx
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { User, Session } from '@supabase/supabase-js';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
loading: boolean;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Get initial session
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// Listen for changes
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
(_event, session) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
}
|
||||
);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const signOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, session, loading, signOut }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Protected Routes (Middleware)
|
||||
|
||||
```typescript
|
||||
// middleware.ts
|
||||
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
let response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return request.cookies.get(name)?.value;
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
response.cookies.set({ name, value, ...options });
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
response.cookies.set({ name, value: '', ...options });
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
// Protect routes
|
||||
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
|
||||
return NextResponse.redirect(new URL('/login', request.url));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/dashboard/:path*', '/api/protected/:path*']
|
||||
};
|
||||
```
|
||||
|
||||
### 6. Password Reset
|
||||
|
||||
```typescript
|
||||
// Request password reset
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/auth/reset-password`
|
||||
});
|
||||
|
||||
// Update password (after clicking reset link)
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password: 'new-password'
|
||||
});
|
||||
```
|
||||
|
||||
## Auth UI Components
|
||||
|
||||
```typescript
|
||||
// components/auth/LoginForm.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
export function LoginForm() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleLogin}>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
required
|
||||
/>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Loading...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **SSR Auth** - Nutze @supabase/ssr für Server-Side Auth
|
||||
2. **Middleware** - Schütze Routen serverseitig
|
||||
3. **Session Refresh** - Automatisch via Supabase Client
|
||||
4. **Error Handling** - User-freundliche Fehlermeldungen
|
||||
5. **Secure Cookies** - HttpOnly, Secure, SameSite
|
||||
|
||||
---
|
||||
|
||||
Frage den Benutzer: Was möchtest du mit Supabase Auth machen?
|
||||
- Auth Setup implementieren
|
||||
- Login/Signup Forms erstellen
|
||||
- OAuth Provider einrichten
|
||||
- Protected Routes konfigurieren
|
||||
- User Profile Management
|
||||
136
.claude/commands/supabase-db.md
Normal file
136
.claude/commands/supabase-db.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Supabase PostgreSQL Datenbank Assistent
|
||||
|
||||
Du bist ein Experte für Supabase PostgreSQL Datenbank-Operationen. Hilf dem Benutzer bei:
|
||||
|
||||
## Deine Aufgaben
|
||||
|
||||
### 1. Tabellen erstellen
|
||||
Erstelle SQL Migrations mit:
|
||||
- Primärschlüssel (id als UUID mit gen_random_uuid())
|
||||
- created_at und updated_at Timestamps
|
||||
- Row Level Security (RLS) Policies
|
||||
- Indexes für häufig abgefragte Spalten
|
||||
|
||||
### 2. CRUD Operationen
|
||||
Generiere TypeScript Code für:
|
||||
```typescript
|
||||
// SELECT
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.select('*')
|
||||
.eq('column', value);
|
||||
|
||||
// INSERT
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.insert({ column: value })
|
||||
.select();
|
||||
|
||||
// UPDATE
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.update({ column: value })
|
||||
.eq('id', id)
|
||||
.select();
|
||||
|
||||
// DELETE
|
||||
const { error } = await supabase
|
||||
.from('table')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
```
|
||||
|
||||
### 3. Komplexe Queries
|
||||
- JOINs mit foreign key relationships
|
||||
- Aggregationen (count, sum, avg)
|
||||
- Filtering und Sorting
|
||||
- Pagination mit range()
|
||||
|
||||
### 4. RLS Policies
|
||||
```sql
|
||||
-- Enable RLS
|
||||
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy für authentifizierte User
|
||||
CREATE POLICY "Users can view own data"
|
||||
ON table_name FOR SELECT
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Policy für Insert
|
||||
CREATE POLICY "Users can insert own data"
|
||||
ON table_name FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
### 5. TypeScript Types generieren
|
||||
```bash
|
||||
# Supabase CLI installieren
|
||||
pnpm add -D supabase
|
||||
|
||||
# Types generieren
|
||||
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > lib/database.types.ts
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Immer RLS aktivieren** - Niemals Tabellen ohne RLS in Production
|
||||
2. **UUIDs als Primary Keys** - Besser für verteilte Systeme
|
||||
3. **Timestamps** - created_at/updated_at für Audit Trail
|
||||
4. **Soft Deletes** - deleted_at statt hartem DELETE
|
||||
5. **Indexes** - Für WHERE und ORDER BY Spalten
|
||||
|
||||
## Beispiel Migration
|
||||
|
||||
```sql
|
||||
-- Create users profile table
|
||||
CREATE TABLE profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
|
||||
username TEXT UNIQUE,
|
||||
full_name TEXT,
|
||||
avatar_url TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policies
|
||||
CREATE POLICY "Public profiles are viewable by everyone"
|
||||
ON profiles FOR SELECT
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "Users can update own profile"
|
||||
ON profiles FOR UPDATE
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Index
|
||||
CREATE INDEX profiles_user_id_idx ON profiles(user_id);
|
||||
|
||||
-- Updated_at trigger
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER profiles_updated_at
|
||||
BEFORE UPDATE ON profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Frage den Benutzer: Was möchtest du mit der Datenbank machen?
|
||||
- Neue Tabelle erstellen
|
||||
- Query schreiben
|
||||
- RLS Policy einrichten
|
||||
- Types generieren
|
||||
- Migration erstellen
|
||||
199
.claude/commands/supabase-storage.md
Normal file
199
.claude/commands/supabase-storage.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Supabase Storage Assistent
|
||||
|
||||
Du bist ein Experte für Supabase Storage. Hilf dem Benutzer bei File-Upload, Download und Bucket-Management.
|
||||
|
||||
## Deine Aufgaben
|
||||
|
||||
### 1. Bucket erstellen
|
||||
|
||||
```sql
|
||||
-- Via SQL (Supabase Dashboard)
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('avatars', 'avatars', true);
|
||||
|
||||
-- Oder via CLI
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Via JavaScript (Admin/Service Role)
|
||||
const { data, error } = await supabaseAdmin.storage.createBucket('avatars', {
|
||||
public: true,
|
||||
fileSizeLimit: 1024 * 1024 * 2, // 2MB
|
||||
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp']
|
||||
});
|
||||
```
|
||||
|
||||
### 2. File Upload
|
||||
|
||||
```typescript
|
||||
// Client-side Upload
|
||||
async function uploadFile(file: File, bucket: string, path: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(path, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Mit Progress
|
||||
async function uploadWithProgress(file: File, bucket: string, path: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(path, file, {
|
||||
onUploadProgress: (progress) => {
|
||||
const percent = (progress.loaded / progress.total) * 100;
|
||||
console.log(`Upload: ${percent.toFixed(0)}%`);
|
||||
}
|
||||
});
|
||||
return { data, error };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. File Download
|
||||
|
||||
```typescript
|
||||
// Download als Blob
|
||||
const { data, error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.download('path/to/file.pdf');
|
||||
|
||||
// Public URL (für öffentliche Buckets)
|
||||
const { data } = supabase.storage
|
||||
.from('bucket')
|
||||
.getPublicUrl('path/to/file.jpg');
|
||||
|
||||
// Signed URL (für private Buckets)
|
||||
const { data, error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.createSignedUrl('path/to/file.pdf', 3600); // 1 Stunde gültig
|
||||
```
|
||||
|
||||
### 4. File Management
|
||||
|
||||
```typescript
|
||||
// Liste Dateien
|
||||
const { data, error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.list('folder/', {
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
sortBy: { column: 'name', order: 'asc' }
|
||||
});
|
||||
|
||||
// Datei löschen
|
||||
const { error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.remove(['path/to/file1.jpg', 'path/to/file2.jpg']);
|
||||
|
||||
// Datei verschieben/umbenennen
|
||||
const { error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.move('old/path.jpg', 'new/path.jpg');
|
||||
|
||||
// Datei kopieren
|
||||
const { error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.copy('source/path.jpg', 'dest/path.jpg');
|
||||
```
|
||||
|
||||
### 5. Storage Policies (RLS)
|
||||
|
||||
```sql
|
||||
-- Öffentlicher Lesezugriff
|
||||
CREATE POLICY "Public read access"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'public-bucket');
|
||||
|
||||
-- Authentifizierte User können eigene Dateien hochladen
|
||||
CREATE POLICY "Users can upload own files"
|
||||
ON storage.objects FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (
|
||||
bucket_id = 'user-files' AND
|
||||
(storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
-- User können nur eigene Dateien löschen
|
||||
CREATE POLICY "Users can delete own files"
|
||||
ON storage.objects FOR DELETE
|
||||
TO authenticated
|
||||
USING (
|
||||
bucket_id = 'user-files' AND
|
||||
(storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
```
|
||||
|
||||
## React Upload Component
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
interface FileUploadProps {
|
||||
bucket: string;
|
||||
onUpload: (url: string) => void;
|
||||
}
|
||||
|
||||
export function FileUpload({ bucket, onUpload }: FileUploadProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${Date.now()}.${fileExt}`;
|
||||
const filePath = `uploads/${fileName}`;
|
||||
|
||||
const { error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(filePath, file);
|
||||
|
||||
if (error) {
|
||||
console.error('Upload error:', error);
|
||||
} else {
|
||||
const { data } = supabase.storage.from(bucket).getPublicUrl(filePath);
|
||||
onUpload(data.publicUrl);
|
||||
}
|
||||
|
||||
setUploading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleUpload}
|
||||
disabled={uploading}
|
||||
/>
|
||||
{uploading && <span>Uploading...</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Bucket-Struktur planen** - z.B. `{user_id}/{type}/{filename}`
|
||||
2. **File Size Limits setzen** - Verhindert Missbrauch
|
||||
3. **MIME Types einschränken** - Nur erlaubte Dateitypen
|
||||
4. **Signed URLs für private Dateien** - Zeitlich begrenzt
|
||||
5. **CDN nutzen** - Public URLs werden automatisch gecacht
|
||||
|
||||
---
|
||||
|
||||
Frage den Benutzer: Was möchtest du mit Supabase Storage machen?
|
||||
- Neuen Bucket erstellen
|
||||
- Upload Component bauen
|
||||
- Download implementieren
|
||||
- Storage Policies einrichten
|
||||
- Dateien verwalten
|
||||
Reference in New Issue
Block a user