Initial commit from template
This commit is contained in:
114
.claude/skills/dependency-scanner/SKILL.md
Normal file
114
.claude/skills/dependency-scanner/SKILL.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: dependency-scanner
|
||||
description: Scannt npm Dependencies auf bekannte Sicherheitsluecken und veraltete Pakete. Nutze diesen Skill nach npm install, bei neuen Dependencies, oder wenn der User nach "vulnerabilities", "outdated", "npm audit", "dependency check", "pakete pruefen", "sicherheitsluecken" fragt. Gibt Ergebnisse zurueck die im Chat angezeigt werden.
|
||||
---
|
||||
|
||||
# Dependency Security Scanner
|
||||
|
||||
Scannt npm/pnpm Dependencies auf Sicherheitsluecken und veraltete Pakete.
|
||||
|
||||
## Automatische Ausfuehrung
|
||||
|
||||
Fuehre diesen Scan aus:
|
||||
1. Nach `npm install` oder `pnpm install`
|
||||
2. Bei neuen Dependencies in package.json
|
||||
3. Regelmaessig als Security Check
|
||||
|
||||
## Scripts
|
||||
|
||||
### Vollstaendiger Scan
|
||||
```bash
|
||||
bash scan-deps.sh
|
||||
```
|
||||
|
||||
### Nur Audit (Vulnerabilities)
|
||||
```bash
|
||||
bash audit.sh
|
||||
```
|
||||
|
||||
### Veraltete Pakete pruefen
|
||||
```bash
|
||||
bash check-outdated.sh
|
||||
```
|
||||
|
||||
### Automatisch fixen
|
||||
```bash
|
||||
bash fix-vulnerabilities.sh
|
||||
```
|
||||
|
||||
## Scan Ablauf
|
||||
|
||||
### 1. npm audit durchfuehren
|
||||
```bash
|
||||
# Mit pnpm
|
||||
pnpm audit --json
|
||||
|
||||
# Mit npm
|
||||
npm audit --json
|
||||
```
|
||||
|
||||
### 2. Veraltete Pakete pruefen
|
||||
```bash
|
||||
# Mit pnpm
|
||||
pnpm outdated --json
|
||||
|
||||
# Mit npm
|
||||
npm outdated --json
|
||||
```
|
||||
|
||||
### 3. License Check (optional)
|
||||
```bash
|
||||
npx license-checker --summary
|
||||
```
|
||||
|
||||
## Report Format fuer Chat
|
||||
|
||||
Gib das Ergebnis in diesem Format zurueck:
|
||||
|
||||
```markdown
|
||||
## Dependency Security Report
|
||||
|
||||
**Scan Datum**: [timestamp]
|
||||
**Pakete geprueft**: [count]
|
||||
|
||||
### Vulnerabilities
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| Critical | 0 |
|
||||
| High | 2 |
|
||||
| Moderate | 5 |
|
||||
| Low | 3 |
|
||||
|
||||
### Kritische Issues
|
||||
|
||||
1. **lodash** (4.17.15) - Prototype Pollution
|
||||
- Fix: `pnpm update lodash`
|
||||
|
||||
2. **axios** (0.21.0) - SSRF Vulnerability
|
||||
- Fix: `pnpm update axios`
|
||||
|
||||
### Veraltete Pakete
|
||||
|
||||
| Paket | Aktuell | Neueste | Typ |
|
||||
|-------|---------|---------|-----|
|
||||
| react | 18.2.0 | 19.0.0 | major |
|
||||
| next | 14.0.0 | 15.1.0 | major |
|
||||
|
||||
### Empfehlungen
|
||||
|
||||
1. **Sofort**: Kritische Vulnerabilities fixen
|
||||
2. **Diese Woche**: High Severity fixen
|
||||
3. **Geplant**: Major Updates evaluieren
|
||||
|
||||
### Automatischer Fix
|
||||
|
||||
Fuehre aus: `pnpm audit fix` oder `bash fix-vulnerabilities.sh`
|
||||
```
|
||||
|
||||
## Wichtig
|
||||
|
||||
- Zeige Ergebnisse IMMER im Chat an
|
||||
- Bei kritischen Vulnerabilities: Warnung hervorheben
|
||||
- Schlage konkrete Fix-Befehle vor
|
||||
- Bei Major Updates: Changelog verlinken
|
||||
16
.claude/skills/dependency-scanner/audit.sh
Executable file
16
.claude/skills/dependency-scanner/audit.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Schneller Vulnerability Audit
|
||||
|
||||
set -e
|
||||
|
||||
# Package Manager erkennen
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
echo "Fuehre pnpm audit aus..."
|
||||
pnpm audit
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
echo "Fuehre yarn audit aus..."
|
||||
yarn audit
|
||||
else
|
||||
echo "Fuehre npm audit aus..."
|
||||
npm audit
|
||||
fi
|
||||
22
.claude/skills/dependency-scanner/check-outdated.sh
Executable file
22
.claude/skills/dependency-scanner/check-outdated.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Prueft auf veraltete Pakete
|
||||
|
||||
set -e
|
||||
|
||||
echo "Pruefe veraltete Pakete..."
|
||||
echo ""
|
||||
|
||||
# Package Manager erkennen
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
pnpm outdated || true
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
yarn outdated || true
|
||||
else
|
||||
npm outdated || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Zum Updaten:"
|
||||
echo " Alle: pnpm update"
|
||||
echo " Einzeln: pnpm update <paket>"
|
||||
echo " Major: pnpm update <paket>@latest"
|
||||
60
.claude/skills/dependency-scanner/fix-vulnerabilities.sh
Executable file
60
.claude/skills/dependency-scanner/fix-vulnerabilities.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# Automatischer Fix fuer Vulnerabilities
|
||||
|
||||
set -e
|
||||
|
||||
# Farben
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "Dependency Vulnerability Fix"
|
||||
echo "============================"
|
||||
echo ""
|
||||
|
||||
# Package Manager erkennen
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
PKG_MANAGER="pnpm"
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
PKG_MANAGER="yarn"
|
||||
else
|
||||
PKG_MANAGER="npm"
|
||||
fi
|
||||
|
||||
# Vorher: Audit Status
|
||||
echo -e "${YELLOW}Status vor Fix:${NC}"
|
||||
$PKG_MANAGER audit 2>/dev/null | tail -5 || true
|
||||
echo ""
|
||||
|
||||
# Fix durchfuehren
|
||||
echo -e "${YELLOW}Fuehre automatischen Fix durch...${NC}"
|
||||
echo ""
|
||||
|
||||
case "$PKG_MANAGER" in
|
||||
"pnpm")
|
||||
# pnpm hat kein direktes audit fix
|
||||
echo "pnpm: Update betroffene Pakete..."
|
||||
pnpm update
|
||||
;;
|
||||
"yarn")
|
||||
yarn audit fix || yarn upgrade
|
||||
;;
|
||||
"npm")
|
||||
npm audit fix
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
|
||||
# Nachher: Audit Status
|
||||
echo -e "${YELLOW}Status nach Fix:${NC}"
|
||||
$PKG_MANAGER audit 2>/dev/null | tail -5 || true
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Fix abgeschlossen!${NC}"
|
||||
echo ""
|
||||
echo "Naechste Schritte:"
|
||||
echo " 1. Teste die Anwendung: pnpm dev"
|
||||
echo " 2. Fuehre Tests aus: pnpm test"
|
||||
echo " 3. Bei Problemen: git checkout package.json pnpm-lock.yaml"
|
||||
160
.claude/skills/dependency-scanner/scan-deps.sh
Executable file
160
.claude/skills/dependency-scanner/scan-deps.sh
Executable file
@@ -0,0 +1,160 @@
|
||||
#!/bin/bash
|
||||
# Vollstaendiger Dependency Security Scan
|
||||
|
||||
set -e
|
||||
|
||||
# Farben
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo -e "${BLUE}Dependency Security Scanner${NC}"
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Package Manager erkennen
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
PKG_MANAGER="pnpm"
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
PKG_MANAGER="yarn"
|
||||
elif [ -f "package-lock.json" ]; then
|
||||
PKG_MANAGER="npm"
|
||||
else
|
||||
PKG_MANAGER="npm"
|
||||
fi
|
||||
|
||||
echo "Package Manager: $PKG_MANAGER"
|
||||
echo ""
|
||||
|
||||
# Temporaere Dateien
|
||||
AUDIT_FILE=$(mktemp)
|
||||
OUTDATED_FILE=$(mktemp)
|
||||
|
||||
# ================================
|
||||
# 1. VULNERABILITY AUDIT
|
||||
# ================================
|
||||
echo -e "${YELLOW}[1/3] Pruefe Sicherheitsluecken...${NC}"
|
||||
|
||||
case "$PKG_MANAGER" in
|
||||
"pnpm")
|
||||
pnpm audit --json > "$AUDIT_FILE" 2>/dev/null || true
|
||||
;;
|
||||
"yarn")
|
||||
yarn audit --json > "$AUDIT_FILE" 2>/dev/null || true
|
||||
;;
|
||||
"npm")
|
||||
npm audit --json > "$AUDIT_FILE" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Audit Ergebnisse parsen
|
||||
if [ -s "$AUDIT_FILE" ]; then
|
||||
CRITICAL=$(jq '.metadata.vulnerabilities.critical // 0' "$AUDIT_FILE" 2>/dev/null || echo "0")
|
||||
HIGH=$(jq '.metadata.vulnerabilities.high // 0' "$AUDIT_FILE" 2>/dev/null || echo "0")
|
||||
MODERATE=$(jq '.metadata.vulnerabilities.moderate // 0' "$AUDIT_FILE" 2>/dev/null || echo "0")
|
||||
LOW=$(jq '.metadata.vulnerabilities.low // 0' "$AUDIT_FILE" 2>/dev/null || echo "0")
|
||||
TOTAL=$((CRITICAL + HIGH + MODERATE + LOW))
|
||||
else
|
||||
CRITICAL=0
|
||||
HIGH=0
|
||||
MODERATE=0
|
||||
LOW=0
|
||||
TOTAL=0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Vulnerabilities gefunden:"
|
||||
echo -e " ${RED}Critical: $CRITICAL${NC}"
|
||||
echo -e " ${RED}High: $HIGH${NC}"
|
||||
echo -e " ${YELLOW}Moderate: $MODERATE${NC}"
|
||||
echo -e " Low: $LOW"
|
||||
echo ""
|
||||
|
||||
# ================================
|
||||
# 2. OUTDATED CHECK
|
||||
# ================================
|
||||
echo -e "${YELLOW}[2/3] Pruefe veraltete Pakete...${NC}"
|
||||
|
||||
case "$PKG_MANAGER" in
|
||||
"pnpm")
|
||||
pnpm outdated --json > "$OUTDATED_FILE" 2>/dev/null || true
|
||||
;;
|
||||
"npm")
|
||||
npm outdated --json > "$OUTDATED_FILE" 2>/dev/null || true
|
||||
;;
|
||||
"yarn")
|
||||
yarn outdated --json > "$OUTDATED_FILE" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -s "$OUTDATED_FILE" ]; then
|
||||
OUTDATED_COUNT=$(jq 'length' "$OUTDATED_FILE" 2>/dev/null || echo "0")
|
||||
echo "Veraltete Pakete: $OUTDATED_COUNT"
|
||||
|
||||
if [ "$OUTDATED_COUNT" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Top 10 veraltete Pakete:"
|
||||
jq -r 'to_entries | .[:10][] | " \(.key): \(.value.current // "?") -> \(.value.latest // "?")"' "$OUTDATED_FILE" 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
OUTDATED_COUNT=0
|
||||
echo "Keine veralteten Pakete gefunden."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ================================
|
||||
# 3. SCORE BERECHNEN
|
||||
# ================================
|
||||
echo -e "${YELLOW}[3/3] Berechne Security Score...${NC}"
|
||||
|
||||
SCORE=$((100 - (CRITICAL * 25) - (HIGH * 10) - (MODERATE * 3) - (LOW * 1)))
|
||||
if [ $SCORE -lt 0 ]; then SCORE=0; fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo -e "${BLUE}ERGEBNIS${NC}"
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $SCORE -ge 90 ]; then
|
||||
echo -e "Security Score: ${GREEN}$SCORE/100${NC}"
|
||||
elif [ $SCORE -ge 70 ]; then
|
||||
echo -e "Security Score: ${YELLOW}$SCORE/100${NC}"
|
||||
else
|
||||
echo -e "Security Score: ${RED}$SCORE/100${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Zusammenfassung:"
|
||||
echo " Vulnerabilities: $TOTAL"
|
||||
echo " Veraltete Pakete: $OUTDATED_COUNT"
|
||||
|
||||
# ================================
|
||||
# EMPFEHLUNGEN
|
||||
# ================================
|
||||
echo ""
|
||||
echo -e "${BLUE}Empfehlungen:${NC}"
|
||||
|
||||
if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then
|
||||
echo -e " ${RED}DRINGEND: Fuehre '$PKG_MANAGER audit fix' aus${NC}"
|
||||
fi
|
||||
|
||||
if [ "$OUTDATED_COUNT" -gt 10 ]; then
|
||||
echo -e " ${YELLOW}Updates verfuegbar: '$PKG_MANAGER update'${NC}"
|
||||
fi
|
||||
|
||||
if [ "$TOTAL" -eq 0 ] && [ "$OUTDATED_COUNT" -lt 5 ]; then
|
||||
echo -e " ${GREEN}Alles in Ordnung! Dependencies sind sicher.${NC}"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$AUDIT_FILE" "$OUTDATED_FILE"
|
||||
|
||||
# Exit mit Fehler bei kritischen Issues
|
||||
if [ "$CRITICAL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
55
.claude/skills/postgres-connect/SKILL.md
Normal file
55
.claude/skills/postgres-connect/SKILL.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: postgres-connect
|
||||
description: Verbindet mit der PostgreSQL Datenbank. Erkennt das Betriebssystem, installiert psql CLI falls noetig, liest DATABASE_URL aus .env und stellt Verbindung her. Nutze diesen Skill wenn der User "Datenbank verbinden", "DB Verbindung", "psql", "PostgreSQL connect" oder aehnliches erwaehnt.
|
||||
---
|
||||
|
||||
# PostgreSQL Datenbank Verbindung
|
||||
|
||||
Dieser Skill verbindet automatisch mit der PostgreSQL Datenbank des Projekts.
|
||||
|
||||
## Automatischer Ablauf
|
||||
|
||||
1. **OS erkennen** mit dem Script `detect-os.sh`
|
||||
2. **psql pruefen** - ist PostgreSQL CLI installiert?
|
||||
3. **Falls nicht installiert** - Installation je nach OS
|
||||
4. **DATABASE_URL laden** aus `.env.local` oder `.env`
|
||||
5. **Verbindung herstellen** mit psql
|
||||
|
||||
## Scripts verwenden
|
||||
|
||||
### OS-Erkennung
|
||||
```bash
|
||||
bash detect-os.sh
|
||||
```
|
||||
|
||||
### psql Installation
|
||||
```bash
|
||||
bash install-psql.sh
|
||||
```
|
||||
|
||||
### Verbindung herstellen
|
||||
```bash
|
||||
bash connect.sh
|
||||
```
|
||||
|
||||
## Manuelle Befehle nach Verbindung
|
||||
|
||||
```sql
|
||||
-- Alle Tabellen anzeigen
|
||||
\dt
|
||||
|
||||
-- Tabellen-Schema
|
||||
\d table_name
|
||||
|
||||
-- Supabase Auth Users
|
||||
SELECT id, email, created_at FROM auth.users LIMIT 10;
|
||||
|
||||
-- RLS Policies pruefen
|
||||
SELECT * FROM pg_policies WHERE schemaname = 'public';
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Connection refused**: Supabase Projekt evtl. pausiert
|
||||
- **SSL required**: `?sslmode=require` an URL anhaengen
|
||||
- **Auth failed**: Passwort URL-encoded? (@ -> %40)
|
||||
36
.claude/skills/postgres-connect/check-connection.sh
Executable file
36
.claude/skills/postgres-connect/check-connection.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# Testet die Datenbankverbindung ohne interaktive Shell
|
||||
|
||||
set -e
|
||||
|
||||
# DATABASE_URL laden
|
||||
if [ -f .env.local ]; then
|
||||
DATABASE_URL=$(grep -E "^DATABASE_URL=" .env.local | cut -d '=' -f2- | tr -d '"' | tr -d "'")
|
||||
fi
|
||||
|
||||
if [ -z "$DATABASE_URL" ] && [ -f .env ]; then
|
||||
DATABASE_URL=$(grep -E "^DATABASE_URL=" .env | cut -d '=' -f2- | tr -d '"' | tr -d "'")
|
||||
fi
|
||||
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "DATABASE_URL nicht gefunden"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verbindung testen
|
||||
echo "Teste Verbindung..."
|
||||
if psql "$DATABASE_URL" -c "SELECT 1;" > /dev/null 2>&1; then
|
||||
echo "Verbindung erfolgreich!"
|
||||
|
||||
# Zusaetzliche Infos
|
||||
echo ""
|
||||
echo "Datenbank-Info:"
|
||||
psql "$DATABASE_URL" -c "SELECT current_database() as database, current_user as user, version();" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "Tabellen:"
|
||||
psql "$DATABASE_URL" -c "\dt" 2>/dev/null || echo "(keine Tabellen gefunden)"
|
||||
else
|
||||
echo "Verbindung fehlgeschlagen!"
|
||||
exit 1
|
||||
fi
|
||||
27
.claude/skills/postgres-connect/connect.sh
Executable file
27
.claude/skills/postgres-connect/connect.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Verbindet mit der PostgreSQL Datenbank aus .env
|
||||
|
||||
set -e
|
||||
|
||||
# DATABASE_URL aus .env.local oder .env laden
|
||||
if [ -f .env.local ]; then
|
||||
DATABASE_URL=$(grep -E "^DATABASE_URL=" .env.local | cut -d '=' -f2- | tr -d '"' | tr -d "'")
|
||||
fi
|
||||
|
||||
if [ -z "$DATABASE_URL" ] && [ -f .env ]; then
|
||||
DATABASE_URL=$(grep -E "^DATABASE_URL=" .env | cut -d '=' -f2- | tr -d '"' | tr -d "'")
|
||||
fi
|
||||
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "Fehler: DATABASE_URL nicht in .env.local oder .env gefunden."
|
||||
echo ""
|
||||
echo "Bitte fuege DATABASE_URL zu .env.local hinzu:"
|
||||
echo 'DATABASE_URL="postgresql://user:password@host:port/database"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Verbinde mit Datenbank..."
|
||||
echo "URL: ${DATABASE_URL%%@*}@***" # URL ohne Passwort ausgeben
|
||||
|
||||
# Verbindung herstellen
|
||||
psql "$DATABASE_URL"
|
||||
28
.claude/skills/postgres-connect/detect-os.sh
Executable file
28
.claude/skills/postgres-connect/detect-os.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# Erkennt das Betriebssystem und gibt es aus
|
||||
|
||||
OS_TYPE=$(uname -s)
|
||||
|
||||
case "$OS_TYPE" in
|
||||
"Darwin")
|
||||
echo "macos"
|
||||
;;
|
||||
"Linux")
|
||||
# Unterscheide zwischen verschiedenen Linux-Distributionen
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
echo "alpine"
|
||||
elif [ -f /etc/debian_version ]; then
|
||||
echo "debian"
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
echo "redhat"
|
||||
else
|
||||
echo "linux"
|
||||
fi
|
||||
;;
|
||||
"MINGW"*|"MSYS"*|"CYGWIN"*)
|
||||
echo "windows"
|
||||
;;
|
||||
*)
|
||||
echo "unknown"
|
||||
;;
|
||||
esac
|
||||
72
.claude/skills/postgres-connect/install-psql.sh
Executable file
72
.claude/skills/postgres-connect/install-psql.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Installiert PostgreSQL CLI basierend auf dem Betriebssystem
|
||||
|
||||
set -e
|
||||
|
||||
# OS erkennen
|
||||
OS_TYPE=$(uname -s)
|
||||
|
||||
# Pruefen ob psql bereits installiert ist
|
||||
if command -v psql &> /dev/null; then
|
||||
echo "psql ist bereits installiert: $(psql --version)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "psql nicht gefunden. Starte Installation..."
|
||||
|
||||
case "$OS_TYPE" in
|
||||
"Darwin")
|
||||
# macOS - mit Homebrew
|
||||
if ! command -v brew &> /dev/null; then
|
||||
echo "Homebrew nicht gefunden. Bitte installiere Homebrew zuerst:"
|
||||
echo '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||
exit 1
|
||||
fi
|
||||
echo "Installiere PostgreSQL via Homebrew..."
|
||||
brew install postgresql
|
||||
;;
|
||||
|
||||
"Linux")
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
# Alpine Linux
|
||||
echo "Installiere PostgreSQL Client auf Alpine..."
|
||||
apk add --no-cache postgresql-client
|
||||
|
||||
elif [ -f /etc/debian_version ]; then
|
||||
# Debian/Ubuntu
|
||||
echo "Installiere PostgreSQL Client auf Debian/Ubuntu..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y postgresql-client
|
||||
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
# RHEL/CentOS/Fedora
|
||||
echo "Installiere PostgreSQL Client auf RHEL/CentOS..."
|
||||
sudo yum install -y postgresql
|
||||
|
||||
else
|
||||
echo "Unbekannte Linux-Distribution. Bitte installiere postgresql-client manuell."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
"MINGW"*|"MSYS"*|"CYGWIN"*)
|
||||
# Windows
|
||||
echo "Windows erkannt."
|
||||
echo "Bitte installiere PostgreSQL von: https://www.postgresql.org/download/windows/"
|
||||
echo "Oder nutze: winget install PostgreSQL.PostgreSQL"
|
||||
exit 1
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unbekanntes Betriebssystem: $OS_TYPE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verifizieren
|
||||
if command -v psql &> /dev/null; then
|
||||
echo "Installation erfolgreich: $(psql --version)"
|
||||
else
|
||||
echo "Installation fehlgeschlagen."
|
||||
exit 1
|
||||
fi
|
||||
148
.claude/skills/semgrep-security/SKILL.md
Normal file
148
.claude/skills/semgrep-security/SKILL.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
name: semgrep-security
|
||||
description: Security Scanner mit Semgrep. WICHTIG - Dieser Skill wird automatisch bei JEDER Code-Implementierung aktiviert. Scannt neuen und geaenderten Code auf Sicherheitsluecken, Vulnerabilities und Best Practice Verstoesse. Aktiviert bei Begriffen wie "implementieren", "code schreiben", "feature erstellen", "komponente bauen", "funktion erstellen", "api route", "fertig", "done", "commit", "security", "scan", "vulnerabilities", "sicherheit".
|
||||
---
|
||||
|
||||
# Semgrep Security Scanner
|
||||
|
||||
Dieser Skill scannt automatisch Code auf Sicherheitsprobleme nach jeder Implementierung.
|
||||
|
||||
## WICHTIG: Automatische Ausfuehrung
|
||||
|
||||
Nach JEDER Code-Implementierung fuehre folgende Schritte aus:
|
||||
|
||||
1. **Semgrep installieren** (falls nicht vorhanden)
|
||||
2. **Geaenderte Dateien scannen**
|
||||
3. **Probleme fixen oder reporten**
|
||||
|
||||
## Scripts
|
||||
|
||||
### Installation pruefen und durchfuehren
|
||||
```bash
|
||||
bash install-semgrep.sh
|
||||
```
|
||||
|
||||
### Scan durchfuehren
|
||||
```bash
|
||||
# Alle Dateien scannen
|
||||
bash scan.sh
|
||||
|
||||
# Nur bestimmte Dateien scannen
|
||||
bash scan.sh app/api/users/route.ts components/UserForm.tsx
|
||||
```
|
||||
|
||||
### Autofix anwenden
|
||||
```bash
|
||||
bash autofix.sh
|
||||
```
|
||||
|
||||
## Workflow nach Implementierung
|
||||
|
||||
### 1. Installation sicherstellen
|
||||
```bash
|
||||
# Pruefen ob semgrep installiert ist
|
||||
which semgrep || bash install-semgrep.sh
|
||||
```
|
||||
|
||||
### 2. Geaenderte Dateien ermitteln
|
||||
```bash
|
||||
# Untracked und modified files
|
||||
git status --porcelain | grep -E '^\?\?|^ M|^M' | cut -c4-
|
||||
```
|
||||
|
||||
### 3. Security Scan ausfuehren
|
||||
```bash
|
||||
# Mit Auto-Config (empfohlen)
|
||||
semgrep --config=auto --json .
|
||||
|
||||
# Fuer TypeScript/React spezifisch
|
||||
semgrep --config=p/typescript --config=p/react --json .
|
||||
|
||||
# OWASP Top 10
|
||||
semgrep --config=p/owasp-top-ten --json .
|
||||
```
|
||||
|
||||
### 4. Ergebnisse analysieren und fixen
|
||||
```bash
|
||||
# Mit Autofix
|
||||
semgrep --config=auto --autofix .
|
||||
|
||||
# Nur Report ohne Fix
|
||||
semgrep --config=auto --sarif -o results.sarif .
|
||||
```
|
||||
|
||||
## Haeufige Security Issues und Fixes
|
||||
|
||||
### SQL Injection
|
||||
```typescript
|
||||
// SCHLECHT
|
||||
const query = `SELECT * FROM users WHERE id = ${userId}`;
|
||||
|
||||
// GUT
|
||||
const { data } = await supabase.from('users').select('*').eq('id', userId);
|
||||
```
|
||||
|
||||
### XSS (Cross-Site Scripting)
|
||||
```typescript
|
||||
// SCHLECHT
|
||||
<div dangerouslySetInnerHTML={{ __html: userInput }} />
|
||||
|
||||
// GUT
|
||||
<div>{sanitize(userInput)}</div>
|
||||
```
|
||||
|
||||
### Hardcoded Secrets
|
||||
```typescript
|
||||
// SCHLECHT
|
||||
const apiKey = "sk-1234567890";
|
||||
|
||||
// GUT
|
||||
const apiKey = process.env.API_KEY;
|
||||
```
|
||||
|
||||
### Insecure Direct Object Reference
|
||||
```typescript
|
||||
// SCHLECHT - Keine Auth-Pruefung
|
||||
const user = await getUser(req.query.id);
|
||||
|
||||
// GUT - Mit RLS oder Auth-Check
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const profile = await supabase.from('profiles').select().eq('user_id', user.id);
|
||||
```
|
||||
|
||||
## Report Format
|
||||
|
||||
Nach jedem Scan zeige:
|
||||
|
||||
```markdown
|
||||
## Security Scan Ergebnisse
|
||||
|
||||
**Score**: 85/100
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| Critical | 0 |
|
||||
| High | 1 |
|
||||
| Medium | 3 |
|
||||
| Low | 5 |
|
||||
|
||||
### Gefundene Issues
|
||||
|
||||
1. **[HIGH] Potential XSS** in `components/Comment.tsx:42`
|
||||
- Problem: Unescaped user input
|
||||
- Fix: Verwende DOMPurify oder sanitize-html
|
||||
|
||||
### Automatisch gefixt
|
||||
- 2 Issues wurden automatisch behoben
|
||||
|
||||
### Naechste Schritte
|
||||
- [ ] Issue #1 manuell pruefen
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Immer scannen** nach Code-Aenderungen
|
||||
2. **Autofix nutzen** fuer einfache Issues
|
||||
3. **Kritische Issues** sofort beheben
|
||||
4. **False Positives** in `.semgrepignore` ausschliessen
|
||||
5. **CI/CD Integration** fuer automatische Scans
|
||||
54
.claude/skills/semgrep-security/autofix.sh
Executable file
54
.claude/skills/semgrep-security/autofix.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Fuehrt Semgrep Autofix durch
|
||||
|
||||
set -e
|
||||
|
||||
# Farben
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Pruefen ob Semgrep installiert ist
|
||||
if ! command -v semgrep &> /dev/null; then
|
||||
echo -e "${YELLOW}Semgrep nicht gefunden. Installiere...${NC}"
|
||||
bash "$(dirname "$0")/install-semgrep.sh"
|
||||
fi
|
||||
|
||||
# Argumente
|
||||
if [ $# -gt 0 ]; then
|
||||
SCAN_TARGET="$@"
|
||||
else
|
||||
SCAN_TARGET="."
|
||||
fi
|
||||
|
||||
echo "Starte Semgrep Autofix..."
|
||||
echo "========================="
|
||||
echo ""
|
||||
|
||||
# Autofix durchfuehren
|
||||
# Nur Rules mit Autofix-Support
|
||||
semgrep \
|
||||
--config=auto \
|
||||
--autofix \
|
||||
--dryrun \
|
||||
$SCAN_TARGET 2>&1 | head -50
|
||||
|
||||
echo ""
|
||||
read -p "Fixes anwenden? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Wende Fixes an..."
|
||||
semgrep \
|
||||
--config=auto \
|
||||
--autofix \
|
||||
$SCAN_TARGET
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Fixes angewendet!${NC}"
|
||||
echo ""
|
||||
echo "Bitte pruefen Sie die Aenderungen:"
|
||||
echo " git diff"
|
||||
else
|
||||
echo "Autofix abgebrochen."
|
||||
fi
|
||||
86
.claude/skills/semgrep-security/install-semgrep.sh
Executable file
86
.claude/skills/semgrep-security/install-semgrep.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# Installiert Semgrep basierend auf dem Betriebssystem
|
||||
|
||||
set -e
|
||||
|
||||
echo "Pruefe Semgrep Installation..."
|
||||
|
||||
# Pruefen ob bereits installiert
|
||||
if command -v semgrep &> /dev/null; then
|
||||
echo "Semgrep ist bereits installiert: $(semgrep --version)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Semgrep nicht gefunden. Starte Installation..."
|
||||
|
||||
# OS erkennen
|
||||
OS_TYPE=$(uname -s)
|
||||
|
||||
case "$OS_TYPE" in
|
||||
"Darwin")
|
||||
# macOS
|
||||
echo "macOS erkannt - nutze Homebrew"
|
||||
if command -v brew &> /dev/null; then
|
||||
brew install semgrep
|
||||
else
|
||||
echo "Homebrew nicht gefunden - nutze pip3"
|
||||
pip3 install semgrep
|
||||
fi
|
||||
;;
|
||||
|
||||
"Linux")
|
||||
echo "Linux erkannt"
|
||||
|
||||
# Versuche verschiedene Paketmanager
|
||||
if command -v apt-get &> /dev/null; then
|
||||
echo "Nutze apt-get..."
|
||||
# Semgrep via Python da apt Version oft veraltet
|
||||
pip3 install semgrep
|
||||
|
||||
elif command -v apk &> /dev/null; then
|
||||
echo "Alpine erkannt - nutze pip3"
|
||||
apk add --no-cache python3 py3-pip
|
||||
pip3 install semgrep
|
||||
|
||||
elif command -v yum &> /dev/null; then
|
||||
echo "RHEL/CentOS erkannt - nutze pip3"
|
||||
pip3 install semgrep
|
||||
|
||||
else
|
||||
echo "Nutze pip3 als Fallback"
|
||||
pip3 install semgrep
|
||||
fi
|
||||
;;
|
||||
|
||||
"MINGW"*|"MSYS"*|"CYGWIN"*)
|
||||
# Windows
|
||||
echo "Windows erkannt"
|
||||
if command -v choco &> /dev/null; then
|
||||
choco install semgrep -y
|
||||
elif command -v pip3 &> /dev/null; then
|
||||
pip3 install semgrep
|
||||
elif command -v pip &> /dev/null; then
|
||||
pip install semgrep
|
||||
else
|
||||
echo "Bitte installiere Python und pip: https://www.python.org/downloads/"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unbekanntes OS: $OS_TYPE - nutze pip3"
|
||||
pip3 install semgrep
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verifizieren
|
||||
if command -v semgrep &> /dev/null; then
|
||||
echo ""
|
||||
echo "Installation erfolgreich!"
|
||||
semgrep --version
|
||||
else
|
||||
echo ""
|
||||
echo "Installation fehlgeschlagen."
|
||||
echo "Bitte manuell installieren: https://semgrep.dev/docs/getting-started/"
|
||||
exit 1
|
||||
fi
|
||||
91
.claude/skills/semgrep-security/rules/next-security.yaml
Normal file
91
.claude/skills/semgrep-security/rules/next-security.yaml
Normal file
@@ -0,0 +1,91 @@
|
||||
# Custom Semgrep Rules fuer Next.js Security
|
||||
|
||||
rules:
|
||||
# Verhindere dangerouslySetInnerHTML ohne Sanitization
|
||||
- id: next-xss-dangerous-html
|
||||
patterns:
|
||||
- pattern: dangerouslySetInnerHTML={{ __html: $VAR }}
|
||||
- pattern-not: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize($VAR) }}
|
||||
- pattern-not: dangerouslySetInnerHTML={{ __html: sanitize($VAR) }}
|
||||
message: "XSS Risiko: Verwende DOMPurify.sanitize() bevor du dangerouslySetInnerHTML nutzt"
|
||||
severity: ERROR
|
||||
languages: [typescript, javascript]
|
||||
|
||||
# Verhindere hardcoded API Keys
|
||||
- id: next-hardcoded-api-key
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: |
|
||||
$KEY = "sk-..."
|
||||
- pattern: |
|
||||
$KEY = "pk_..."
|
||||
- pattern: |
|
||||
apiKey: "..."
|
||||
- pattern: |
|
||||
api_key = "..."
|
||||
message: "Hardcoded API Key gefunden. Verwende Environment Variables."
|
||||
severity: ERROR
|
||||
languages: [typescript, javascript]
|
||||
|
||||
# Verhindere ungeschuetzte API Routes
|
||||
- id: next-unprotected-api-route
|
||||
patterns:
|
||||
- pattern: |
|
||||
export async function $METHOD(request: NextRequest) {
|
||||
...
|
||||
$DB.$OPERATION(...)
|
||||
...
|
||||
}
|
||||
- pattern-not: |
|
||||
export async function $METHOD(request: NextRequest) {
|
||||
...
|
||||
auth.getUser()
|
||||
...
|
||||
}
|
||||
- pattern-not: |
|
||||
export async function $METHOD(request: NextRequest) {
|
||||
...
|
||||
getSession()
|
||||
...
|
||||
}
|
||||
message: "API Route ohne Authentication. Fuege Auth-Check hinzu."
|
||||
severity: WARNING
|
||||
languages: [typescript]
|
||||
paths:
|
||||
include:
|
||||
- "app/api/**"
|
||||
|
||||
# Verhindere Secrets in Client Components
|
||||
- id: next-secret-in-client
|
||||
patterns:
|
||||
- pattern-inside: |
|
||||
"use client"
|
||||
...
|
||||
- pattern-either:
|
||||
- pattern: process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
- pattern: process.env.DATABASE_URL
|
||||
- pattern: process.env.$SECRET_KEY
|
||||
message: "Server-only Secret in Client Component. Verschiebe in Server Component oder API Route."
|
||||
severity: ERROR
|
||||
languages: [typescript, javascript]
|
||||
|
||||
# Verhindere eval und new Function
|
||||
- id: next-no-eval
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: eval(...)
|
||||
- pattern: new Function(...)
|
||||
message: "eval() und new Function() sind Sicherheitsrisiken. Finde eine Alternative."
|
||||
severity: ERROR
|
||||
languages: [typescript, javascript]
|
||||
|
||||
# Supabase RLS Check
|
||||
- id: supabase-rls-bypass
|
||||
patterns:
|
||||
- pattern: supabaseAdmin.from($TABLE)
|
||||
- pattern-not-inside: |
|
||||
// RLS bypassed intentionally
|
||||
...
|
||||
message: "supabaseAdmin umgeht RLS. Stelle sicher dass dies beabsichtigt ist."
|
||||
severity: WARNING
|
||||
languages: [typescript, javascript]
|
||||
29
.claude/skills/semgrep-security/scan-changed.sh
Executable file
29
.claude/skills/semgrep-security/scan-changed.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Scannt nur geaenderte Dateien (staged + unstaged)
|
||||
|
||||
set -e
|
||||
|
||||
# Farben
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "Ermittle geaenderte Dateien..."
|
||||
|
||||
# Geaenderte Dateien ermitteln (TypeScript/JavaScript)
|
||||
CHANGED_FILES=$(git status --porcelain 2>/dev/null | \
|
||||
grep -E '\.(ts|tsx|js|jsx)$' | \
|
||||
sed 's/^...//' | \
|
||||
tr '\n' ' ')
|
||||
|
||||
if [ -z "$CHANGED_FILES" ]; then
|
||||
echo -e "${GREEN}Keine geaenderten TypeScript/JavaScript Dateien gefunden.${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Geaenderte Dateien:"
|
||||
echo "$CHANGED_FILES" | tr ' ' '\n' | grep -v '^$'
|
||||
echo ""
|
||||
|
||||
# Scan durchfuehren
|
||||
bash "$(dirname "$0")/scan.sh" $CHANGED_FILES
|
||||
89
.claude/skills/semgrep-security/scan.sh
Executable file
89
.claude/skills/semgrep-security/scan.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
# Fuehrt Semgrep Security Scan durch
|
||||
|
||||
set -e
|
||||
|
||||
# Farben fuer Output
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Pruefen ob Semgrep installiert ist
|
||||
if ! command -v semgrep &> /dev/null; then
|
||||
echo -e "${YELLOW}Semgrep nicht gefunden. Installiere...${NC}"
|
||||
bash "$(dirname "$0")/install-semgrep.sh"
|
||||
fi
|
||||
|
||||
# Argumente: spezifische Dateien oder alles scannen
|
||||
if [ $# -gt 0 ]; then
|
||||
SCAN_TARGET="$@"
|
||||
echo "Scanne spezifische Dateien: $SCAN_TARGET"
|
||||
else
|
||||
SCAN_TARGET="."
|
||||
echo "Scanne gesamtes Projekt..."
|
||||
fi
|
||||
|
||||
# Temporaere Datei fuer JSON Output
|
||||
RESULT_FILE=$(mktemp)
|
||||
|
||||
# Scan durchfuehren
|
||||
echo ""
|
||||
echo "Starte Security Scan..."
|
||||
echo "========================"
|
||||
|
||||
semgrep \
|
||||
--config=auto \
|
||||
--config=p/security-audit \
|
||||
--config=p/typescript \
|
||||
--json \
|
||||
--output="$RESULT_FILE" \
|
||||
$SCAN_TARGET 2>/dev/null || true
|
||||
|
||||
# Ergebnisse parsen
|
||||
if [ -f "$RESULT_FILE" ]; then
|
||||
# Anzahl der Findings zaehlen
|
||||
TOTAL=$(jq '.results | length' "$RESULT_FILE" 2>/dev/null || echo "0")
|
||||
ERRORS=$(jq '[.results[] | select(.extra.severity == "ERROR")] | length' "$RESULT_FILE" 2>/dev/null || echo "0")
|
||||
WARNINGS=$(jq '[.results[] | select(.extra.severity == "WARNING")] | length' "$RESULT_FILE" 2>/dev/null || echo "0")
|
||||
INFO=$(jq '[.results[] | select(.extra.severity == "INFO")] | length' "$RESULT_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
echo ""
|
||||
echo "========================"
|
||||
echo "Scan Ergebnisse"
|
||||
echo "========================"
|
||||
echo ""
|
||||
|
||||
if [ "$TOTAL" -eq 0 ]; then
|
||||
echo -e "${GREEN}Keine Sicherheitsprobleme gefunden!${NC}"
|
||||
else
|
||||
echo -e "Gefunden: ${RED}$ERRORS Critical/High${NC}, ${YELLOW}$WARNINGS Medium${NC}, $INFO Low"
|
||||
echo ""
|
||||
|
||||
# Details ausgeben
|
||||
echo "Details:"
|
||||
echo "--------"
|
||||
jq -r '.results[] | "[\(.extra.severity)] \(.check_id)\n File: \(.path):\(.start.line)\n Message: \(.extra.message)\n"' "$RESULT_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Score berechnen
|
||||
SCORE=$((100 - (ERRORS * 10) - (WARNINGS * 3) - (INFO * 1)))
|
||||
if [ $SCORE -lt 0 ]; then SCORE=0; fi
|
||||
|
||||
echo ""
|
||||
echo "========================"
|
||||
echo -e "Security Score: ${GREEN}$SCORE/100${NC}"
|
||||
echo "========================"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$RESULT_FILE"
|
||||
|
||||
# Exit mit Fehler wenn kritische Issues
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "${RED}WARNUNG: Es wurden kritische Sicherheitsprobleme gefunden!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}Scan abgeschlossen - keine Ergebnisdatei erstellt${NC}"
|
||||
fi
|
||||
152
.claude/skills/supabase-auth/SKILL.md
Normal file
152
.claude/skills/supabase-auth/SKILL.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
name: supabase-auth
|
||||
description: Supabase Authentication Setup und User Management. Nutze diesen Skill fuer Login, Signup, OAuth, Sessions, Password Reset und geschuetzte Routen. Aktiviert bei Begriffen wie "Login", "Anmeldung", "Registrierung", "Signup", "Auth", "Authentication", "User", "Benutzer", "Session", "Passwort", "OAuth", "Google Login", "geschuetzt", "protected route".
|
||||
---
|
||||
|
||||
# Supabase Authentication Skill
|
||||
|
||||
Dieser Skill hilft bei Authentication mit Supabase.
|
||||
|
||||
## Auth Client Setup
|
||||
|
||||
```typescript
|
||||
// lib/supabase.ts - Browser Client
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
export const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
// lib/supabase-admin.ts - Server Client
|
||||
export const supabaseAdmin = createClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Sign Up / Sign In
|
||||
|
||||
```typescript
|
||||
// Email/Password Signup
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email: 'user@example.com',
|
||||
password: 'secure-password',
|
||||
options: {
|
||||
data: { full_name: 'Max Mustermann' }
|
||||
}
|
||||
});
|
||||
|
||||
// Email/Password Login
|
||||
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`
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
```typescript
|
||||
// Aktuelle Session
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
// Aktueller User
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
// Auth State Listener
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
console.log(event, session);
|
||||
});
|
||||
|
||||
// Logout
|
||||
await supabase.auth.signOut();
|
||||
```
|
||||
|
||||
## Password Reset
|
||||
|
||||
```typescript
|
||||
// Reset anfordern
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/auth/reset-password`
|
||||
});
|
||||
|
||||
// Neues Passwort setzen
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password: 'new-password'
|
||||
});
|
||||
```
|
||||
|
||||
## Middleware (Protected Routes)
|
||||
|
||||
```typescript
|
||||
// middleware.ts
|
||||
import { createServerClient } from '@supabase/ssr';
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
let response = NextResponse.next({ request });
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get: (name) => request.cookies.get(name)?.value,
|
||||
set: (name, value, options) => {
|
||||
response.cookies.set({ name, value, ...options });
|
||||
},
|
||||
remove: (name, options) => {
|
||||
response.cookies.set({ name, value: '', ...options });
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
// Redirect wenn nicht eingeloggt
|
||||
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*']
|
||||
};
|
||||
```
|
||||
|
||||
## Auth Context
|
||||
|
||||
Siehe `templates/AuthContext.tsx` fuer einen kompletten Auth Provider.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. SSR Auth mit @supabase/ssr
|
||||
2. Middleware fuer Route Protection
|
||||
3. Service Role Key nur serverseitig
|
||||
4. User-freundliche Fehlermeldungen
|
||||
5. Email Verification aktivieren
|
||||
108
.claude/skills/supabase-auth/templates/AuthContext.tsx
Normal file
108
.claude/skills/supabase-auth/templates/AuthContext.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { User, Session, AuthError } from '@supabase/supabase-js';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
loading: boolean;
|
||||
signUp: (email: string, password: string, metadata?: Record<string, unknown>) => Promise<{ error: AuthError | null }>;
|
||||
signIn: (email: string, password: string) => Promise<{ error: AuthError | null }>;
|
||||
signInWithOAuth: (provider: 'google' | 'github' | 'azure') => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
resetPassword: (email: string) => Promise<{ error: AuthError | null }>;
|
||||
}
|
||||
|
||||
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(() => {
|
||||
// Initiale Session holen
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// Auth State Listener
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
(_event, session) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
}
|
||||
);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const signUp = useCallback(
|
||||
async (email: string, password: string, metadata?: Record<string, unknown>) => {
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: { data: metadata },
|
||||
});
|
||||
return { error };
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const signIn = useCallback(async (email: string, password: string) => {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
return { error };
|
||||
}, []);
|
||||
|
||||
const signInWithOAuth = useCallback(async (provider: 'google' | 'github' | 'azure') => {
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
await supabase.auth.signOut();
|
||||
}, []);
|
||||
|
||||
const resetPassword = useCallback(async (email: string) => {
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/auth/reset-password`,
|
||||
});
|
||||
return { error };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
session,
|
||||
loading,
|
||||
signUp,
|
||||
signIn,
|
||||
signInWithOAuth,
|
||||
signOut,
|
||||
resetPassword,
|
||||
}}
|
||||
>
|
||||
{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;
|
||||
}
|
||||
123
.claude/skills/supabase-auth/templates/LoginForm.tsx
Normal file
123
.claude/skills/supabase-auth/templates/LoginForm.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
interface LoginFormProps {
|
||||
onSuccess?: () => void;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export function LoginForm({ onSuccess, redirectTo = '/dashboard' }: LoginFormProps) {
|
||||
const { signIn, signInWithOAuth } = useAuth();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const { error } = await signIn(email, password);
|
||||
|
||||
if (error) {
|
||||
setError(getErrorMessage(error.message));
|
||||
setLoading(false);
|
||||
} else {
|
||||
onSuccess?.();
|
||||
window.location.href = redirectTo;
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuth = async (provider: 'google' | 'github') => {
|
||||
setLoading(true);
|
||||
await signInWithOAuth(provider);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-sm">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-1">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium mb-1">
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-2 px-4 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Wird geladen...' : 'Anmelden'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
Oder weiter mit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOAuth('google')}
|
||||
disabled={loading}
|
||||
className="py-2 px-4 border rounded-md hover:bg-muted disabled:opacity-50"
|
||||
>
|
||||
Google
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOAuth('github')}
|
||||
disabled={loading}
|
||||
className="py-2 px-4 border rounded-md hover:bg-muted disabled:opacity-50"
|
||||
>
|
||||
GitHub
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getErrorMessage(message: string): string {
|
||||
const errorMap: Record<string, string> = {
|
||||
'Invalid login credentials': 'Ungueltige Anmeldedaten',
|
||||
'Email not confirmed': 'Bitte bestaetigen Sie zuerst Ihre Email',
|
||||
'Too many requests': 'Zu viele Versuche. Bitte warten Sie einen Moment.',
|
||||
};
|
||||
return errorMap[message] || 'Ein Fehler ist aufgetreten';
|
||||
}
|
||||
110
.claude/skills/supabase-db/SKILL.md
Normal file
110
.claude/skills/supabase-db/SKILL.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: supabase-db
|
||||
description: Supabase PostgreSQL Datenbank Operationen. Nutze diesen Skill fuer CRUD Operationen, SQL Migrations, Tabellen erstellen, RLS Policies, Queries und TypeScript Types generieren. Aktiviert bei Begriffen wie "Datenbank", "Tabelle erstellen", "SQL", "Query", "Migration", "RLS", "Row Level Security", "select", "insert", "update", "delete".
|
||||
---
|
||||
|
||||
# Supabase PostgreSQL Datenbank Skill
|
||||
|
||||
Dieser Skill hilft bei allen Datenbank-Operationen mit Supabase.
|
||||
|
||||
## Tabellen erstellen
|
||||
|
||||
Verwende immer dieses Pattern:
|
||||
|
||||
```sql
|
||||
CREATE TABLE table_name (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
-- deine Spalten hier
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- RLS aktivieren
|
||||
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Updated_at Trigger
|
||||
CREATE TRIGGER table_name_updated_at
|
||||
BEFORE UPDATE ON table_name
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
```
|
||||
|
||||
## CRUD Operationen
|
||||
|
||||
### Select
|
||||
```typescript
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.select('*')
|
||||
.eq('column', value)
|
||||
.order('created_at', { ascending: false });
|
||||
```
|
||||
|
||||
### Insert
|
||||
```typescript
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.insert({ column: value })
|
||||
.select()
|
||||
.single();
|
||||
```
|
||||
|
||||
### Update
|
||||
```typescript
|
||||
const { data, error } = await supabase
|
||||
.from('table')
|
||||
.update({ column: newValue })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
```
|
||||
|
||||
### Delete
|
||||
```typescript
|
||||
const { error } = await supabase
|
||||
.from('table')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
```
|
||||
|
||||
## RLS Policies
|
||||
|
||||
```sql
|
||||
-- Lesen: Jeder authentifizierte User
|
||||
CREATE POLICY "Users can read own data"
|
||||
ON table_name FOR SELECT
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Schreiben: Nur eigene Daten
|
||||
CREATE POLICY "Users can insert own data"
|
||||
ON table_name FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Update: Nur eigene Daten
|
||||
CREATE POLICY "Users can update own data"
|
||||
ON table_name FOR UPDATE
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Delete: Nur eigene Daten
|
||||
CREATE POLICY "Users can delete own data"
|
||||
ON table_name FOR DELETE
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
## TypeScript Types generieren
|
||||
|
||||
```bash
|
||||
npx supabase gen types typescript --project-id PROJECT_ID > lib/database.types.ts
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Immer UUIDs als Primary Key
|
||||
2. Immer created_at/updated_at
|
||||
3. Immer RLS aktivieren
|
||||
4. Soft Deletes mit deleted_at
|
||||
5. Indexes fuer WHERE/ORDER BY Spalten
|
||||
84
.claude/skills/supabase-db/templates/migration.sql
Normal file
84
.claude/skills/supabase-db/templates/migration.sql
Normal file
@@ -0,0 +1,84 @@
|
||||
-- Migration: {{MIGRATION_NAME}}
|
||||
-- Created: {{DATE}}
|
||||
|
||||
-- ===========================================
|
||||
-- Tabelle erstellen
|
||||
-- ===========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS {{TABLE_NAME}} (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Deine Spalten hier
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- Foreign Keys (optional)
|
||||
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- ===========================================
|
||||
-- Row Level Security
|
||||
-- ===========================================
|
||||
|
||||
ALTER TABLE {{TABLE_NAME}} ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Select Policy
|
||||
CREATE POLICY "{{TABLE_NAME}}_select_policy"
|
||||
ON {{TABLE_NAME}} FOR SELECT
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Insert Policy
|
||||
CREATE POLICY "{{TABLE_NAME}}_insert_policy"
|
||||
ON {{TABLE_NAME}} FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Update Policy
|
||||
CREATE POLICY "{{TABLE_NAME}}_update_policy"
|
||||
ON {{TABLE_NAME}} FOR UPDATE
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Delete Policy
|
||||
CREATE POLICY "{{TABLE_NAME}}_delete_policy"
|
||||
ON {{TABLE_NAME}} FOR DELETE
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ===========================================
|
||||
-- Indexes
|
||||
-- ===========================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS {{TABLE_NAME}}_user_id_idx ON {{TABLE_NAME}}(user_id);
|
||||
CREATE INDEX IF NOT EXISTS {{TABLE_NAME}}_created_at_idx ON {{TABLE_NAME}}(created_at DESC);
|
||||
|
||||
-- ===========================================
|
||||
-- Updated_at Trigger
|
||||
-- ===========================================
|
||||
|
||||
-- Funktion (nur einmal pro DB noetig)
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger
|
||||
CREATE TRIGGER {{TABLE_NAME}}_updated_at
|
||||
BEFORE UPDATE ON {{TABLE_NAME}}
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ===========================================
|
||||
-- Grants (optional)
|
||||
-- ===========================================
|
||||
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON {{TABLE_NAME}} TO authenticated;
|
||||
-- GRANT SELECT ON {{TABLE_NAME}} TO anon;
|
||||
142
.claude/skills/supabase-storage/SKILL.md
Normal file
142
.claude/skills/supabase-storage/SKILL.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
name: supabase-storage
|
||||
description: Supabase Storage fuer Datei-Upload und Download. Nutze diesen Skill fuer Bucket-Erstellung, File Upload, Download, Signed URLs und Storage Policies. Aktiviert bei Begriffen wie "Upload", "Download", "Datei", "File", "Bild hochladen", "Storage", "Bucket", "S3", "Bilder", "Avatar", "Dokumente".
|
||||
---
|
||||
|
||||
# Supabase Storage Skill
|
||||
|
||||
Dieser Skill hilft bei allen Storage-Operationen mit Supabase.
|
||||
|
||||
## Bucket erstellen
|
||||
|
||||
```typescript
|
||||
// Server-side mit Admin Client
|
||||
const { data, error } = await supabaseAdmin.storage.createBucket('avatars', {
|
||||
public: true,
|
||||
fileSizeLimit: 1024 * 1024 * 2, // 2MB
|
||||
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp']
|
||||
});
|
||||
```
|
||||
|
||||
Oder via SQL:
|
||||
```sql
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'avatars',
|
||||
'avatars',
|
||||
true,
|
||||
2097152,
|
||||
ARRAY['image/png', 'image/jpeg', 'image/webp']
|
||||
);
|
||||
```
|
||||
|
||||
## File Upload
|
||||
|
||||
```typescript
|
||||
async function uploadFile(file: File, bucket: string, path: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(path, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: false // true = ueberschreiben erlaubt
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Public URL holen
|
||||
const { data: urlData } = supabase.storage
|
||||
.from(bucket)
|
||||
.getPublicUrl(path);
|
||||
|
||||
return urlData.publicUrl;
|
||||
}
|
||||
```
|
||||
|
||||
## File Download
|
||||
|
||||
```typescript
|
||||
// Als Blob
|
||||
const { data, error } = await supabase.storage
|
||||
.from('documents')
|
||||
.download('path/to/file.pdf');
|
||||
|
||||
// Public URL (oeffentliche Buckets)
|
||||
const { data } = supabase.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl('user123/avatar.jpg');
|
||||
|
||||
// Signed URL (private Buckets, zeitlich begrenzt)
|
||||
const { data, error } = await supabase.storage
|
||||
.from('private-docs')
|
||||
.createSignedUrl('secret.pdf', 3600); // 1 Stunde
|
||||
```
|
||||
|
||||
## File Management
|
||||
|
||||
```typescript
|
||||
// Liste Dateien
|
||||
const { data, error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.list('folder/', {
|
||||
limit: 100,
|
||||
sortBy: { column: 'created_at', order: 'desc' }
|
||||
});
|
||||
|
||||
// Loeschen
|
||||
const { error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.remove(['path/file1.jpg', 'path/file2.jpg']);
|
||||
|
||||
// Verschieben
|
||||
const { error } = await supabase.storage
|
||||
.from('bucket')
|
||||
.move('old/path.jpg', 'new/path.jpg');
|
||||
```
|
||||
|
||||
## Storage Policies (RLS)
|
||||
|
||||
```sql
|
||||
-- User koennen eigene Dateien hochladen
|
||||
CREATE POLICY "Users 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 koennen eigene Dateien lesen
|
||||
CREATE POLICY "Users read own files"
|
||||
ON storage.objects FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
bucket_id = 'user-files' AND
|
||||
(storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
-- User koennen eigene Dateien loeschen
|
||||
CREATE POLICY "Users delete own files"
|
||||
ON storage.objects FOR DELETE
|
||||
TO authenticated
|
||||
USING (
|
||||
bucket_id = 'user-files' AND
|
||||
(storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
-- Oeffentlicher Lesezugriff
|
||||
CREATE POLICY "Public read"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'public-assets');
|
||||
```
|
||||
|
||||
## React Upload Component
|
||||
|
||||
Siehe `templates/FileUpload.tsx` fuer eine fertige Component.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Ordnerstruktur: `{user_id}/{type}/{filename}`
|
||||
2. File Size Limits setzen
|
||||
3. MIME Types einschraenken
|
||||
4. Signed URLs fuer private Dateien
|
||||
5. Unique Filenames (z.B. mit Timestamp)
|
||||
126
.claude/skills/supabase-storage/templates/FileUpload.tsx
Normal file
126
.claude/skills/supabase-storage/templates/FileUpload.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
interface FileUploadProps {
|
||||
bucket: string;
|
||||
folder?: string;
|
||||
accept?: string;
|
||||
maxSize?: number; // in bytes
|
||||
onUpload: (url: string, path: string) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export function FileUpload({
|
||||
bucket,
|
||||
folder = 'uploads',
|
||||
accept = 'image/*',
|
||||
maxSize = 5 * 1024 * 1024, // 5MB default
|
||||
onUpload,
|
||||
onError,
|
||||
}: FileUploadProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
|
||||
const uploadFile = useCallback(
|
||||
async (file: File) => {
|
||||
// Validierung
|
||||
if (file.size > maxSize) {
|
||||
onError?.(new Error(`Datei zu gross. Maximum: ${maxSize / 1024 / 1024}MB`));
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
setProgress(0);
|
||||
|
||||
try {
|
||||
// Unique filename generieren
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${Date.now()}-${Math.random().toString(36).substring(2)}.${fileExt}`;
|
||||
const filePath = `${folder}/${fileName}`;
|
||||
|
||||
// Upload
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(filePath, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
});
|
||||
|
||||
if (uploadError) throw uploadError;
|
||||
|
||||
// URL holen
|
||||
const { data } = supabase.storage.from(bucket).getPublicUrl(filePath);
|
||||
|
||||
onUpload(data.publicUrl, filePath);
|
||||
setProgress(100);
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
onError?.(error instanceof Error ? error : new Error('Upload fehlgeschlagen'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
},
|
||||
[bucket, folder, maxSize, onUpload, onError]
|
||||
);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) uploadFile(file);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) uploadFile(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
relative border-2 border-dashed rounded-lg p-6 text-center
|
||||
transition-colors cursor-pointer
|
||||
${dragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
|
||||
${uploading ? 'pointer-events-none opacity-50' : 'hover:border-primary'}
|
||||
`}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(true);
|
||||
}}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept={accept}
|
||||
onChange={handleChange}
|
||||
disabled={uploading}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
|
||||
{uploading ? (
|
||||
<div className="space-y-2">
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Uploading...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">
|
||||
Datei hierher ziehen oder klicken zum Auswaehlen
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Max. {maxSize / 1024 / 1024}MB
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user