Initial commit from template
This commit is contained in:
147
.claude/CLAUDE.md
Normal file
147
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Lumina Project - Claude Code Kontext
|
||||
|
||||
## Projekt-Typ
|
||||
Full-Stack Web-Applikation erstellt mit dem Lumina AI Development Platform Template.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** Next.js 15 (App Router)
|
||||
- **Runtime:** Node.js 20+
|
||||
- **Sprache:** TypeScript (strict mode)
|
||||
- **Styling:** Tailwind CSS
|
||||
- **UI Components:** Spartan UI
|
||||
- **Backend/Database:** Supabase (PostgreSQL)
|
||||
- **Auth:** Supabase Auth
|
||||
- **Storage:** Supabase Storage
|
||||
- **Package Manager:** pnpm (bevorzugt) oder npm
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # API Routes
|
||||
│ ├── (auth)/ # Auth-geschützte Routen
|
||||
│ ├── layout.tsx # Root Layout
|
||||
│ ├── page.tsx # Homepage
|
||||
│ └── globals.css # Globale Styles
|
||||
├── components/ # React Components
|
||||
│ ├── ui/ # Basis UI Components
|
||||
│ └── ... # Feature Components
|
||||
├── lib/ # Utilities & Services
|
||||
│ ├── supabase.ts # Supabase Client
|
||||
│ └── utils.ts # Helper Functions
|
||||
├── public/ # Static Assets
|
||||
├── helm/ # Kubernetes Helm Charts
|
||||
└── .claude/ # Claude Code Konfiguration
|
||||
```
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
### TypeScript
|
||||
- Strict mode aktiviert
|
||||
- Keine `any` Types - immer typisieren
|
||||
- Interfaces für Objekte, Types für Unions/Primitives
|
||||
- Barrel Exports aus index.ts vermeiden
|
||||
|
||||
### React/Next.js
|
||||
- Server Components als Default
|
||||
- "use client" nur wenn nötig (interaktive Components)
|
||||
- App Router Patterns verwenden
|
||||
- Async Components für Data Fetching
|
||||
|
||||
### Styling
|
||||
- Tailwind CSS für alle Styles
|
||||
- Keine inline styles
|
||||
- cn() Helper für conditional classes
|
||||
- Spartan UI Components wenn verfügbar
|
||||
|
||||
### Supabase
|
||||
- Server-Side: Service Role Key für Admin-Operationen
|
||||
- Client-Side: Anon Key für User-Operationen
|
||||
- Row Level Security (RLS) immer aktivieren
|
||||
- Typen aus Database generieren
|
||||
|
||||
## Wichtige Patterns
|
||||
|
||||
### API Routes
|
||||
```typescript
|
||||
// app/api/example/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Logic here
|
||||
return NextResponse.json({ data });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Supabase Server Client
|
||||
```typescript
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
// Server-side mit Service Role
|
||||
const supabaseAdmin = createClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||||
);
|
||||
```
|
||||
|
||||
### Supabase Client
|
||||
```typescript
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
// Client-side Query
|
||||
const { data, error } = await supabase
|
||||
.from('table_name')
|
||||
.select('*')
|
||||
.eq('column', 'value');
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Erforderlich:
|
||||
- `NEXT_PUBLIC_SUPABASE_URL` - Supabase Project URL
|
||||
- `NEXT_PUBLIC_SUPABASE_ANON_KEY` - Supabase Anonymous Key
|
||||
- `SUPABASE_SERVICE_ROLE_KEY` - Supabase Service Role Key (Server-only)
|
||||
- `DATABASE_URL` - PostgreSQL Connection String
|
||||
|
||||
Optional:
|
||||
- `REDIS_URL` - Redis Cache
|
||||
- `ANTHROPIC_API_KEY` - Claude API
|
||||
- `OPENAI_API_KEY` - OpenAI API
|
||||
|
||||
## NPM Scripts
|
||||
|
||||
- `pnpm dev` - Development Server (Port 3000)
|
||||
- `pnpm build` - Production Build
|
||||
- `pnpm start` - Production Server
|
||||
- `pnpm lint` - ESLint Check
|
||||
- `pnpm format` - Prettier Format
|
||||
|
||||
## Deployment
|
||||
|
||||
Das Projekt unterstützt:
|
||||
- **Docker:** Dockerfile vorhanden
|
||||
- **Kubernetes:** Helm Charts in `/helm`
|
||||
- **Harbor:** Image Registry Integration
|
||||
|
||||
## Slash Commands
|
||||
|
||||
Verfügbare Claude Code Commands:
|
||||
- `/supabase-db` - Datenbank-Operationen (CRUD, Migrations, Queries)
|
||||
- `/supabase-storage` - Storage-Operationen (Upload, Download, Buckets)
|
||||
- `/supabase-auth` - Authentication Setup & User Management
|
||||
- `/component` - Neue UI Component erstellen
|
||||
- `/api` - Neue API Route erstellen
|
||||
- `/deploy` - Deployment Anleitung
|
||||
|
||||
## Hinweise
|
||||
|
||||
1. **Sicherheit:** Niemals Service Role Key im Client-Code verwenden
|
||||
2. **RLS:** Immer Row Level Security Policies definieren
|
||||
3. **Typen:** Supabase Types mit `supabase gen types` generieren
|
||||
4. **Migrations:** SQL Migrations in Supabase Dashboard oder via CLI
|
||||
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
|
||||
23
.claude/settings.json
Normal file
23
.claude/settings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pnpm *)",
|
||||
"Bash(npm *)",
|
||||
"Bash(npx *)",
|
||||
"Bash(docker *)",
|
||||
"Bash(docker-compose *)",
|
||||
"Bash(git *)",
|
||||
"Bash(curl *)",
|
||||
"Bash(ls *)",
|
||||
"Bash(cat *)",
|
||||
"Bash(mkdir *)",
|
||||
"Bash(cp *)",
|
||||
"Bash(mv *)",
|
||||
"WebSearch"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(rm -rf /)",
|
||||
"Bash(sudo *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
76
.dockerignore
Normal file
76
.dockerignore
Normal file
@@ -0,0 +1,76 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
out
|
||||
build
|
||||
dist
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Debug
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
docs/
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
.travis.yml
|
||||
azure-pipelines.yml
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose*.yml
|
||||
|
||||
# Kubernetes
|
||||
helm/
|
||||
k8s/
|
||||
*.yaml
|
||||
*.yml
|
||||
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# Supabase
|
||||
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
36
.env.local.example
Normal file
36
.env.local.example
Normal file
@@ -0,0 +1,36 @@
|
||||
# Application Configuration
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Database (Supabase or local PostgreSQL)
|
||||
DATABASE_URL="postgresql://lumina:lumina_dev_password@postgres:5432/lumina_db"
|
||||
|
||||
# Supabase Configuration
|
||||
SUPABASE_URL="https://your-project.supabase.co"
|
||||
SUPABASE_ANON_KEY="your-anon-key"
|
||||
SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
|
||||
|
||||
# Authentication
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
NEXTAUTH_SECRET="your-secret-key-here"
|
||||
|
||||
# Redis (Optional - for caching)
|
||||
REDIS_URL="redis://redis:6379"
|
||||
|
||||
# API Keys
|
||||
ANTHROPIC_API_KEY="your-anthropic-key"
|
||||
OPENAI_API_KEY="your-openai-key"
|
||||
|
||||
# Email (Optional)
|
||||
SMTP_HOST="smtp.example.com"
|
||||
SMTP_PORT=587
|
||||
SMTP_USER="your-email@example.com"
|
||||
SMTP_PASSWORD="your-password"
|
||||
SMTP_FROM="noreply@example.com"
|
||||
|
||||
# Storage (Optional)
|
||||
S3_BUCKET="your-bucket"
|
||||
S3_REGION="eu-central-1"
|
||||
S3_ACCESS_KEY="your-access-key"
|
||||
S3_SECRET_KEY="your-secret-key"
|
||||
13
.eslintrc.json
Normal file
13
.eslintrc.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "warn"
|
||||
}
|
||||
}
|
||||
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
232
.lumina/skills/semgrep-security.md
Normal file
232
.lumina/skills/semgrep-security.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# Semgrep Security Skill
|
||||
|
||||
You are a security expert using Semgrep to scan code for vulnerabilities, security issues, and best practice violations.
|
||||
|
||||
## Your Responsibilities
|
||||
|
||||
1. **Auto-Install Semgrep**: Check if Semgrep is installed, if not install it automatically
|
||||
2. **Security Scanning**: Run comprehensive security scans on the codebase
|
||||
3. **Vulnerability Detection**: Identify security vulnerabilities, code smells, and anti-patterns
|
||||
4. **Report Generation**: Provide clear, actionable security reports
|
||||
5. **Fix Suggestions**: Suggest fixes for detected issues
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Check Semgrep Installation
|
||||
```bash
|
||||
semgrep --version
|
||||
```
|
||||
|
||||
### Install Semgrep (if missing)
|
||||
```bash
|
||||
# macOS (OSX) - using Homebrew
|
||||
brew install semgrep
|
||||
|
||||
# Linux - using apt-get
|
||||
sudo apt-get update && sudo apt-get install -y semgrep
|
||||
|
||||
# Windows - using Chocolatey
|
||||
choco install semgrep -y
|
||||
|
||||
# Universal fallback - using pip3 (works on all platforms)
|
||||
pip3 install semgrep
|
||||
```
|
||||
|
||||
### Run Security Scan
|
||||
```bash
|
||||
# Full security scan with all rules
|
||||
semgrep --config=auto --json --output=semgrep-results.json .
|
||||
|
||||
# Quick scan with common security rules
|
||||
semgrep --config=p/security-audit --json .
|
||||
|
||||
# OWASP Top 10 scan
|
||||
semgrep --config=p/owasp-top-ten --json .
|
||||
|
||||
# Language-specific scans
|
||||
semgrep --config=p/typescript --json .
|
||||
semgrep --config=p/react --json .
|
||||
semgrep --config=p/javascript --json .
|
||||
```
|
||||
|
||||
### Scan Specific Files/Directories
|
||||
```bash
|
||||
# Scan specific directory
|
||||
semgrep --config=auto --json app/
|
||||
|
||||
# Scan specific file types
|
||||
semgrep --config=auto --json --include="*.ts" --include="*.tsx" .
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Initial Setup**
|
||||
- Check if Semgrep is installed
|
||||
- If not, install it automatically
|
||||
- Verify installation was successful
|
||||
|
||||
2. **Security Scan**
|
||||
- Run comprehensive scan with `--config=auto`
|
||||
- Focus on high and critical severity issues first
|
||||
- Scan for OWASP Top 10 vulnerabilities
|
||||
|
||||
3. **Analysis**
|
||||
- Parse JSON results
|
||||
- Categorize issues by severity (critical, high, medium, low)
|
||||
- Group by vulnerability type
|
||||
- Identify patterns and recurring issues
|
||||
|
||||
4. **Reporting**
|
||||
- Summarize total issues found
|
||||
- Highlight critical/high severity issues
|
||||
- Provide file paths and line numbers
|
||||
- Include fix suggestions
|
||||
- Calculate security score
|
||||
|
||||
5. **Recommendations**
|
||||
- Prioritize fixes (critical first)
|
||||
- Suggest security best practices
|
||||
- Recommend additional security measures
|
||||
|
||||
## Security Score Calculation
|
||||
|
||||
Calculate a security score based on:
|
||||
- Total issues found
|
||||
- Severity distribution
|
||||
- Lines of code scanned
|
||||
- Issue density (issues per 1000 LOC)
|
||||
|
||||
Formula:
|
||||
```
|
||||
Base Score: 100
|
||||
- Critical Issue: -10 points each
|
||||
- High Issue: -5 points each
|
||||
- Medium Issue: -2 points each
|
||||
- Low Issue: -0.5 points each
|
||||
|
||||
Final Score: max(0, Base Score - Total Deductions)
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
Always provide:
|
||||
|
||||
```markdown
|
||||
## Security Scan Results
|
||||
|
||||
**Scan Date**: [timestamp]
|
||||
**Files Scanned**: [count]
|
||||
**Security Score**: [0-100] 🛡️
|
||||
|
||||
### Summary
|
||||
- 🔴 Critical: [count]
|
||||
- 🟠 High: [count]
|
||||
- 🟡 Medium: [count]
|
||||
- 🟢 Low: [count]
|
||||
|
||||
### Critical Issues (if any)
|
||||
1. **[Vulnerability Type]** in `[file]:[line]`
|
||||
- **Issue**: [description]
|
||||
- **Fix**: [suggestion]
|
||||
|
||||
### Recommendations
|
||||
- [Priority 1 action]
|
||||
- [Priority 2 action]
|
||||
- ...
|
||||
|
||||
### Next Steps
|
||||
[What to do next]
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Always run scans from the project root directory
|
||||
- Use `--json` flag for machine-readable output
|
||||
- Focus on actionable issues, filter out false positives
|
||||
- Prioritize security issues that could lead to vulnerabilities
|
||||
- Be concise but thorough in recommendations
|
||||
- Update the security dashboard with latest results
|
||||
|
||||
## Auto-Installation Script
|
||||
|
||||
The Security Dashboard automatically detects your OS and installs Semgrep with the appropriate package manager:
|
||||
|
||||
**Automatic Installation:**
|
||||
- **macOS (OSX)**: Uses Homebrew (`brew install semgrep`)
|
||||
- **Linux**: Uses apt-get (`sudo apt-get install semgrep`), fallback to pip3
|
||||
- **Windows**: Uses Chocolatey (`choco install semgrep`), fallback to pip3
|
||||
|
||||
If you want to manually install, use this script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "🔍 Checking Semgrep installation..."
|
||||
if ! command -v semgrep &> /dev/null; then
|
||||
echo "📦 Semgrep not found. Detecting OS and installing..."
|
||||
|
||||
# Detect OS
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
echo "🍎 Detected macOS - using Homebrew"
|
||||
if command -v brew &> /dev/null; then
|
||||
brew install semgrep
|
||||
else
|
||||
echo "⚠️ Homebrew not found, using pip3 fallback"
|
||||
pip3 install semgrep
|
||||
fi
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
echo "🐧 Detected Linux - using apt-get"
|
||||
if command -v apt-get &> /dev/null; then
|
||||
sudo apt-get update && sudo apt-get install -y semgrep
|
||||
else
|
||||
echo "⚠️ apt-get not found, using pip3 fallback"
|
||||
pip3 install semgrep
|
||||
fi
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
|
||||
# Windows
|
||||
echo "🪟 Detected Windows - using Chocolatey"
|
||||
if command -v choco &> /dev/null; then
|
||||
choco install semgrep -y
|
||||
else
|
||||
echo "⚠️ Chocolatey not found, using pip3 fallback"
|
||||
pip3 install semgrep
|
||||
fi
|
||||
else
|
||||
# Unknown OS - fallback to pip3
|
||||
echo "❓ Unknown OS - using pip3 fallback"
|
||||
pip3 install semgrep
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
if command -v semgrep &> /dev/null; then
|
||||
echo "✅ Semgrep installed successfully!"
|
||||
semgrep --version
|
||||
else
|
||||
echo "❌ Installation failed. Please install manually."
|
||||
echo "Visit: https://semgrep.dev/docs/getting-started/"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✅ Semgrep is already installed"
|
||||
semgrep --version
|
||||
fi
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
User: "Scan the code for security issues"
|
||||
- Check Semgrep installation
|
||||
- Run security scan with `--config=auto`
|
||||
- Analyze results
|
||||
- Generate report with security score
|
||||
|
||||
User: "Check for OWASP Top 10 vulnerabilities"
|
||||
- Run scan with `--config=p/owasp-top-ten`
|
||||
- Focus on critical web security issues
|
||||
- Provide detailed report
|
||||
|
||||
User: "What's our security score?"
|
||||
- Run quick scan
|
||||
- Calculate security score
|
||||
- Show summary dashboard
|
||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
76
.semgrepignore
Normal file
76
.semgrepignore
Normal file
@@ -0,0 +1,76 @@
|
||||
# Semgrep Ignore Configuration
|
||||
# Files and directories to exclude from security scanning
|
||||
# This file ensures Semgrep scans ALL subdirectories except the excluded ones
|
||||
|
||||
# Dependencies (CRITICAL - exclude to improve scan performance)
|
||||
node_modules/
|
||||
.pnp/
|
||||
.pnp.js
|
||||
vendor/
|
||||
|
||||
# Build outputs (generated files, not source code)
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
*.d.ts
|
||||
.turbo/
|
||||
|
||||
# Test files (optional - uncomment if you want to scan tests for security)
|
||||
# **/*.test.ts
|
||||
# **/*.test.tsx
|
||||
# **/*.spec.ts
|
||||
# **/*.spec.tsx
|
||||
# __tests__/
|
||||
# __mocks__/
|
||||
# coverage/
|
||||
|
||||
# Configuration files
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# Lock files
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
# Documentation (optional)
|
||||
*.md
|
||||
!SECURITY.md
|
||||
|
||||
# Prisma migrations (generated)
|
||||
prisma/migrations/
|
||||
|
||||
# Public assets
|
||||
public/
|
||||
|
||||
# Storybook
|
||||
.storybook/
|
||||
storybook-static/
|
||||
269
DEPLOYMENT.md
Normal file
269
DEPLOYMENT.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# 🚀 Deployment Guide
|
||||
|
||||
Quick reference for deploying your Lumina application to production.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Docker](#docker)
|
||||
- [Kubernetes](#kubernetes)
|
||||
- [CI/CD](#cicd)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Quick Start
|
||||
|
||||
The fastest way to deploy using Make commands:
|
||||
|
||||
```bash
|
||||
# Build and push Docker image
|
||||
make docker-all
|
||||
|
||||
# Deploy to Kubernetes (fresh install)
|
||||
make deploy-fresh
|
||||
|
||||
# Or upgrade existing deployment
|
||||
make deploy
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
### Build Image
|
||||
|
||||
```bash
|
||||
# Using Makefile
|
||||
make docker-build
|
||||
|
||||
# Or directly
|
||||
docker build -t lumina-app:latest .
|
||||
```
|
||||
|
||||
### Test Locally
|
||||
|
||||
```bash
|
||||
# Using docker-compose (recommended)
|
||||
make compose-up
|
||||
|
||||
# Or run container directly
|
||||
docker run -p 3000:3000 --env-file .env.local lumina-app:latest
|
||||
```
|
||||
|
||||
### Push to Harbor
|
||||
|
||||
```bash
|
||||
# Login to Harbor
|
||||
make docker-login
|
||||
|
||||
# Build and push
|
||||
make docker-all
|
||||
```
|
||||
|
||||
## Kubernetes
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Kubernetes cluster (1.25+)
|
||||
- kubectl configured
|
||||
- Helm 3.x installed
|
||||
- Harbor registry access
|
||||
|
||||
### Deploy Application
|
||||
|
||||
```bash
|
||||
# Full fresh deployment with secrets
|
||||
make deploy-fresh
|
||||
|
||||
# Or step by step:
|
||||
|
||||
# 1. Create namespace
|
||||
make k8s-namespace
|
||||
|
||||
# 2. Create registry secret
|
||||
make k8s-secret-registry
|
||||
|
||||
# 3. Create app secrets (from .env.local)
|
||||
make k8s-secret-app
|
||||
|
||||
# 4. Install with Helm
|
||||
make helm-install
|
||||
```
|
||||
|
||||
### Monitor Deployment
|
||||
|
||||
```bash
|
||||
# View pods
|
||||
make k8s-pods
|
||||
|
||||
# View logs
|
||||
make k8s-logs
|
||||
|
||||
# Port forward to localhost
|
||||
make k8s-port-forward
|
||||
|
||||
# Check Helm status
|
||||
make helm-status
|
||||
```
|
||||
|
||||
### Update Deployment
|
||||
|
||||
```bash
|
||||
# Build new image and upgrade
|
||||
IMAGE_TAG=v1.1.0 make deploy
|
||||
|
||||
# Or just upgrade Helm release
|
||||
make helm-upgrade
|
||||
```
|
||||
|
||||
### Customize Configuration
|
||||
|
||||
Edit `helm/lumina-app/values.yaml` to configure:
|
||||
|
||||
- **Replicas**: Number of pods
|
||||
- **Resources**: CPU/Memory limits
|
||||
- **Autoscaling**: Min/Max replicas
|
||||
- **Ingress**: Domain settings
|
||||
- **Environment**: Custom variables
|
||||
- **Persistence**: Enable storage
|
||||
|
||||
### Rollback
|
||||
|
||||
```bash
|
||||
# Uninstall deployment
|
||||
make helm-uninstall
|
||||
|
||||
# Or rollback to previous version
|
||||
helm rollback $(helm list -n lumina-apps -q) -n lumina-apps
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
### Gitea Integration
|
||||
|
||||
The template supports automated deployments via Gitea Actions:
|
||||
|
||||
1. **Push to main** → Triggers build
|
||||
2. **Build succeeds** → Push to Harbor
|
||||
3. **Harbor webhook** → Update K8s deployment
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Required secrets in Gitea/CI:
|
||||
|
||||
- `HARBOR_USERNAME`: Harbor registry username
|
||||
- `HARBOR_PASSWORD`: Harbor registry password
|
||||
- `KUBECONFIG`: Base64-encoded kubeconfig
|
||||
|
||||
### Manual Workflow
|
||||
|
||||
```bash
|
||||
# 1. Develop locally
|
||||
pnpm dev
|
||||
|
||||
# 2. Test with Docker Compose
|
||||
make compose-up
|
||||
|
||||
# 3. Build and push image
|
||||
make docker-all
|
||||
|
||||
# 4. Deploy to staging
|
||||
NAMESPACE=staging make helm-install
|
||||
|
||||
# 5. Deploy to production
|
||||
NAMESPACE=production make helm-install
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pods not starting
|
||||
|
||||
```bash
|
||||
# Check pod status
|
||||
kubectl get pods -n lumina-apps
|
||||
|
||||
# Describe pod for events
|
||||
kubectl describe pod <pod-name> -n lumina-apps
|
||||
|
||||
# Check logs
|
||||
kubectl logs <pod-name> -n lumina-apps
|
||||
```
|
||||
|
||||
### Image pull errors
|
||||
|
||||
```bash
|
||||
# Verify registry secret exists
|
||||
kubectl get secret harbor-registry-secret -n lumina-apps
|
||||
|
||||
# Test registry access
|
||||
docker login harbor.advisori.de
|
||||
|
||||
# Recreate secret
|
||||
make k8s-secret-registry
|
||||
```
|
||||
|
||||
### Application errors
|
||||
|
||||
```bash
|
||||
# Check application logs
|
||||
make k8s-logs
|
||||
|
||||
# Check environment variables
|
||||
kubectl exec -it <pod-name> -n lumina-apps -- env
|
||||
|
||||
# Verify secrets
|
||||
kubectl get secret lumina-app-secrets -n lumina-apps -o yaml
|
||||
```
|
||||
|
||||
### Health check failures
|
||||
|
||||
```bash
|
||||
# Test health endpoint
|
||||
kubectl port-forward svc/app 3000:80 -n lumina-apps
|
||||
curl http://localhost:3000/api/health
|
||||
|
||||
# Check probe configuration
|
||||
kubectl describe pod <pod-name> -n lumina-apps | grep -A 10 "Liveness\|Readiness"
|
||||
```
|
||||
|
||||
### Ingress issues
|
||||
|
||||
```bash
|
||||
# Check ingress configuration
|
||||
kubectl get ingress -n lumina-apps
|
||||
kubectl describe ingress -n lumina-apps
|
||||
|
||||
# Verify cert-manager certificates
|
||||
kubectl get certificates -n lumina-apps
|
||||
kubectl describe certificate lumina-app-tls -n lumina-apps
|
||||
```
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# Show all available commands
|
||||
make help
|
||||
|
||||
# View Helm values
|
||||
helm get values $(helm list -n lumina-apps -q) -n lumina-apps
|
||||
|
||||
# Update single value
|
||||
helm upgrade <release> ./helm/lumina-app \
|
||||
--namespace lumina-apps \
|
||||
--set image.tag=v1.2.0 \
|
||||
--reuse-values
|
||||
|
||||
# Scale manually
|
||||
kubectl scale deployment <release>-lumina-app --replicas=5 -n lumina-apps
|
||||
|
||||
# View HPA status
|
||||
kubectl get hpa -n lumina-apps
|
||||
kubectl describe hpa <release>-lumina-app -n lumina-apps
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- 📚 [Documentation](https://github.com/advisori/lumina)
|
||||
- 💬 [Support](mailto:info@advisori.de)
|
||||
- 🐛 [Report Issues](https://github.com/advisori/lumina/issues)
|
||||
|
||||
---
|
||||
|
||||
**Generated by Lumina** - AI-Powered Development Platform
|
||||
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
# Multi-stage build for Next.js with optimized production image
|
||||
# Stage 1: Dependencies
|
||||
FROM node:20-alpine AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Stage 2: Builder
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependencies from deps stage
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Set environment variables for build
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Runner
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy necessary files from builder
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
# Set correct permissions
|
||||
RUN chown -R nextjs:nodejs /app
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "server.js"]
|
||||
187
Makefile
Normal file
187
Makefile
Normal file
@@ -0,0 +1,187 @@
|
||||
# Lumina Application Makefile
|
||||
# Simplifies Docker and Kubernetes operations
|
||||
|
||||
# Variables
|
||||
PROJECT_NAME ?= lumina-app
|
||||
PROJECT_ID ?= $(shell basename $(CURDIR))
|
||||
HARBOR_REGISTRY ?= harbor.advisori.de
|
||||
HARBOR_PROJECT ?= lumina
|
||||
IMAGE_NAME ?= $(HARBOR_REGISTRY)/$(HARBOR_PROJECT)/$(PROJECT_ID)
|
||||
IMAGE_TAG ?= latest
|
||||
NAMESPACE ?= lumina-apps
|
||||
HELM_RELEASE ?= $(PROJECT_ID)
|
||||
|
||||
# Colors for output
|
||||
BLUE := \033[0;34m
|
||||
GREEN := \033[0;32m
|
||||
YELLOW := \033[1;33m
|
||||
RED := \033[0;31m
|
||||
NC := \033[0m # No Color
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show this help message
|
||||
@echo "$(BLUE)Lumina Application - Available Commands$(NC)"
|
||||
@echo ""
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-20s$(NC) %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
|
||||
# Docker Commands
|
||||
.PHONY: docker-build
|
||||
docker-build: ## Build Docker image
|
||||
@echo "$(BLUE)Building Docker image...$(NC)"
|
||||
docker build -t $(IMAGE_NAME):$(IMAGE_TAG) .
|
||||
@echo "$(GREEN)✓ Image built: $(IMAGE_NAME):$(IMAGE_TAG)$(NC)"
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run: ## Run Docker container locally
|
||||
@echo "$(BLUE)Starting container...$(NC)"
|
||||
docker run -p 3000:3000 --env-file .env.local --name $(PROJECT_ID) $(IMAGE_NAME):$(IMAGE_TAG)
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: ## Push Docker image to Harbor
|
||||
@echo "$(BLUE)Pushing image to Harbor...$(NC)"
|
||||
docker push $(IMAGE_NAME):$(IMAGE_TAG)
|
||||
@echo "$(GREEN)✓ Image pushed: $(IMAGE_NAME):$(IMAGE_TAG)$(NC)"
|
||||
|
||||
.PHONY: docker-login
|
||||
docker-login: ## Login to Harbor registry
|
||||
@echo "$(BLUE)Logging into Harbor...$(NC)"
|
||||
docker login $(HARBOR_REGISTRY)
|
||||
|
||||
.PHONY: docker-all
|
||||
docker-all: docker-build docker-push ## Build and push Docker image
|
||||
@echo "$(GREEN)✓ Docker build and push complete$(NC)"
|
||||
|
||||
# Docker Compose Commands
|
||||
.PHONY: compose-up
|
||||
compose-up: ## Start services with docker-compose
|
||||
@echo "$(BLUE)Starting services with docker-compose...$(NC)"
|
||||
docker-compose up -d
|
||||
@echo "$(GREEN)✓ Services started$(NC)"
|
||||
|
||||
.PHONY: compose-down
|
||||
compose-down: ## Stop services with docker-compose
|
||||
@echo "$(BLUE)Stopping services...$(NC)"
|
||||
docker-compose down
|
||||
@echo "$(GREEN)✓ Services stopped$(NC)"
|
||||
|
||||
.PHONY: compose-logs
|
||||
compose-logs: ## View docker-compose logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Kubernetes Commands
|
||||
.PHONY: k8s-namespace
|
||||
k8s-namespace: ## Create Kubernetes namespace
|
||||
@echo "$(BLUE)Creating namespace: $(NAMESPACE)$(NC)"
|
||||
kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
|
||||
@echo "$(GREEN)✓ Namespace ready$(NC)"
|
||||
|
||||
.PHONY: k8s-secret-registry
|
||||
k8s-secret-registry: ## Create Harbor registry secret
|
||||
@echo "$(BLUE)Creating registry secret...$(NC)"
|
||||
@read -p "Harbor Username: " HARBOR_USER; \
|
||||
read -sp "Harbor Password: " HARBOR_PASS; \
|
||||
echo ""; \
|
||||
kubectl create secret docker-registry harbor-registry-secret \
|
||||
--docker-server=$(HARBOR_REGISTRY) \
|
||||
--docker-username=$$HARBOR_USER \
|
||||
--docker-password=$$HARBOR_PASS \
|
||||
--namespace=$(NAMESPACE) \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
@echo "$(GREEN)✓ Registry secret created$(NC)"
|
||||
|
||||
.PHONY: k8s-secret-app
|
||||
k8s-secret-app: ## Create application secrets from .env.local
|
||||
@echo "$(BLUE)Creating application secrets...$(NC)"
|
||||
@if [ ! -f .env.local ]; then \
|
||||
echo "$(RED)✗ .env.local not found$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
kubectl create secret generic lumina-app-secrets \
|
||||
--from-env-file=.env.local \
|
||||
--namespace=$(NAMESPACE) \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
@echo "$(GREEN)✓ Application secrets created$(NC)"
|
||||
|
||||
.PHONY: helm-install
|
||||
helm-install: k8s-namespace ## Install application with Helm
|
||||
@echo "$(BLUE)Installing with Helm...$(NC)"
|
||||
helm upgrade --install $(HELM_RELEASE) ./helm/lumina-app \
|
||||
--namespace $(NAMESPACE) \
|
||||
--set image.repository=$(HARBOR_REGISTRY)/$(HARBOR_PROJECT)/$(PROJECT_ID) \
|
||||
--set image.tag=$(IMAGE_TAG) \
|
||||
--set ingress.hosts[0].host=$(PROJECT_ID).advisori.de \
|
||||
--set ingress.tls[0].hosts[0]=$(PROJECT_ID).advisori.de \
|
||||
--create-namespace
|
||||
@echo "$(GREEN)✓ Application deployed$(NC)"
|
||||
|
||||
.PHONY: helm-upgrade
|
||||
helm-upgrade: ## Upgrade Helm release
|
||||
@echo "$(BLUE)Upgrading Helm release...$(NC)"
|
||||
helm upgrade $(HELM_RELEASE) ./helm/lumina-app \
|
||||
--namespace $(NAMESPACE) \
|
||||
--set image.tag=$(IMAGE_TAG) \
|
||||
--reuse-values
|
||||
@echo "$(GREEN)✓ Application upgraded$(NC)"
|
||||
|
||||
.PHONY: helm-uninstall
|
||||
helm-uninstall: ## Uninstall Helm release
|
||||
@echo "$(YELLOW)Uninstalling Helm release...$(NC)"
|
||||
helm uninstall $(HELM_RELEASE) --namespace $(NAMESPACE)
|
||||
@echo "$(GREEN)✓ Application uninstalled$(NC)"
|
||||
|
||||
.PHONY: helm-status
|
||||
helm-status: ## Show Helm release status
|
||||
helm status $(HELM_RELEASE) --namespace $(NAMESPACE)
|
||||
|
||||
# Kubernetes Monitoring
|
||||
.PHONY: k8s-pods
|
||||
k8s-pods: ## Show pods
|
||||
kubectl get pods -n $(NAMESPACE) -l app.kubernetes.io/instance=$(HELM_RELEASE)
|
||||
|
||||
.PHONY: k8s-logs
|
||||
k8s-logs: ## Show application logs
|
||||
kubectl logs -f -n $(NAMESPACE) -l app.kubernetes.io/instance=$(HELM_RELEASE)
|
||||
|
||||
.PHONY: k8s-describe
|
||||
k8s-describe: ## Describe deployment
|
||||
kubectl describe deployment/$(HELM_RELEASE)-lumina-app -n $(NAMESPACE)
|
||||
|
||||
.PHONY: k8s-port-forward
|
||||
k8s-port-forward: ## Port forward to local machine
|
||||
@echo "$(BLUE)Port forwarding to localhost:3000...$(NC)"
|
||||
kubectl port-forward svc/$(HELM_RELEASE)-lumina-app 3000:80 -n $(NAMESPACE)
|
||||
|
||||
# Full Deployment Pipeline
|
||||
.PHONY: deploy
|
||||
deploy: docker-all helm-upgrade ## Full deployment (build, push, upgrade)
|
||||
@echo "$(GREEN)✓ Full deployment complete!$(NC)"
|
||||
|
||||
.PHONY: deploy-fresh
|
||||
deploy-fresh: docker-all k8s-namespace k8s-secret-registry k8s-secret-app helm-install ## Fresh deployment with secrets
|
||||
@echo "$(GREEN)✓ Fresh deployment complete!$(NC)"
|
||||
|
||||
# Cleanup
|
||||
.PHONY: clean
|
||||
clean: ## Clean local Docker images and containers
|
||||
@echo "$(YELLOW)Cleaning up...$(NC)"
|
||||
-docker rm -f $(PROJECT_ID) 2>/dev/null || true
|
||||
-docker rmi $(IMAGE_NAME):$(IMAGE_TAG) 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Cleanup complete$(NC)"
|
||||
|
||||
# Development
|
||||
.PHONY: dev
|
||||
dev: ## Start development server
|
||||
pnpm dev
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build Next.js application
|
||||
pnpm build
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run linter
|
||||
pnpm lint
|
||||
|
||||
.PHONY: format
|
||||
format: ## Format code
|
||||
pnpm format
|
||||
262
README.md
Normal file
262
README.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# asd
|
||||
|
||||
ads
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** Next.js 15 (App Router)
|
||||
- **Language:** TypeScript
|
||||
- **Styling:** Tailwind CSS
|
||||
- **UI Components:** Spartan UI
|
||||
- **Database & Auth:** Supabase
|
||||
- **Code Quality:** ESLint + Prettier
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20 or higher
|
||||
- pnpm 8 or higher (recommended) or npm
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Copy environment variables
|
||||
cp .env.example .env.local
|
||||
|
||||
# Update .env.local with your Supabase credentials
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Run development server
|
||||
pnpm dev
|
||||
|
||||
# Open http://localhost:3000
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Create production build
|
||||
pnpm build
|
||||
|
||||
# Start production server
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
project-1766459980010-q6lr8s01b/
|
||||
├── app/ # Next.js app router pages
|
||||
│ ├── layout.tsx # Root layout
|
||||
│ ├── page.tsx # Homepage
|
||||
│ └── globals.css # Global styles
|
||||
├── components/ # React components
|
||||
│ └── ui/ # UI components
|
||||
├── lib/ # Utility functions
|
||||
│ ├── supabase.ts # Supabase client
|
||||
│ └── utils.ts # Helper functions
|
||||
├── public/ # Static assets
|
||||
└── ...config files
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create a `.env.local` file with:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
- `pnpm dev` - Start development server
|
||||
- `pnpm build` - Create production build
|
||||
- `pnpm start` - Start production server
|
||||
- `pnpm lint` - Run ESLint
|
||||
- `pnpm format` - Format code with Prettier
|
||||
|
||||
## Deployment
|
||||
|
||||
This template comes with production-ready Docker and Kubernetes configurations.
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
#### Build Docker Image
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t lumina-app:latest .
|
||||
|
||||
# Run locally
|
||||
docker run -p 3000:3000 --env-file .env.local lumina-app:latest
|
||||
```
|
||||
|
||||
#### Docker Compose (Recommended for local testing)
|
||||
|
||||
```bash
|
||||
# Start all services (app + postgres + redis)
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f app
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
#### Push to Harbor Registry
|
||||
|
||||
```bash
|
||||
# Tag image for Harbor
|
||||
docker tag lumina-app:latest harbor.advisori.de/lumina/project-1766459980010-q6lr8s01b:latest
|
||||
|
||||
# Login to Harbor
|
||||
docker login harbor.advisori.de
|
||||
|
||||
# Push image
|
||||
docker push harbor.advisori.de/lumina/project-1766459980010-q6lr8s01b:latest
|
||||
```
|
||||
|
||||
### Kubernetes Deployment
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- Kubernetes cluster (1.25+)
|
||||
- kubectl configured
|
||||
- Helm 3.x installed
|
||||
- Harbor registry access
|
||||
|
||||
#### Deploy with Helm
|
||||
|
||||
```bash
|
||||
# Navigate to helm chart
|
||||
cd helm/lumina-app
|
||||
|
||||
# Create namespace
|
||||
kubectl create namespace lumina-apps
|
||||
|
||||
# Create 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
|
||||
|
||||
# Create application secrets
|
||||
kubectl create secret generic lumina-app-secrets \
|
||||
--from-literal=DATABASE_URL="your-database-url" \
|
||||
--from-literal=SUPABASE_URL="your-supabase-url" \
|
||||
--from-literal=SUPABASE_ANON_KEY="your-anon-key" \
|
||||
--namespace=lumina-apps
|
||||
|
||||
# Install/Upgrade with Helm
|
||||
helm upgrade --install project-1766459980010-q6lr8s01b . \
|
||||
--namespace lumina-apps \
|
||||
--set image.repository=harbor.advisori.de/lumina/project-1766459980010-q6lr8s01b \
|
||||
--set image.tag=latest \
|
||||
--set ingress.hosts[0].host=project-1766459980010-q6lr8s01b.advisori.de \
|
||||
--set ingress.tls[0].hosts[0]=project-1766459980010-q6lr8s01b.advisori.de
|
||||
```
|
||||
|
||||
#### Customize Deployment
|
||||
|
||||
Edit `helm/lumina-app/values.yaml` to customize:
|
||||
|
||||
- **Replicas**: Number of pods (default: 2)
|
||||
- **Resources**: CPU/Memory limits
|
||||
- **Autoscaling**: Min/Max replicas and thresholds
|
||||
- **Ingress**: Domain and TLS settings
|
||||
- **Environment**: Add custom env variables
|
||||
- **Persistence**: Enable persistent storage
|
||||
|
||||
#### Monitor Deployment
|
||||
|
||||
```bash
|
||||
# Check pods
|
||||
kubectl get pods -n lumina-apps
|
||||
|
||||
# View logs
|
||||
kubectl logs -f deployment/project-1766459980010-q6lr8s01b -n lumina-apps
|
||||
|
||||
# Check ingress
|
||||
kubectl get ingress -n lumina-apps
|
||||
|
||||
# Port forward for testing
|
||||
kubectl port-forward svc/project-1766459980010-q6lr8s01b 3000:80 -n lumina-apps
|
||||
```
|
||||
|
||||
#### Scaling
|
||||
|
||||
```bash
|
||||
# Manual scaling
|
||||
kubectl scale deployment project-1766459980010-q6lr8s01b --replicas=5 -n lumina-apps
|
||||
|
||||
# Horizontal Pod Autoscaler (enabled by default)
|
||||
kubectl get hpa -n lumina-apps
|
||||
```
|
||||
|
||||
#### Update Deployment
|
||||
|
||||
```bash
|
||||
# Build and push new image
|
||||
docker build -t harbor.advisori.de/lumina/project-1766459980010-q6lr8s01b:v1.1.0 .
|
||||
docker push harbor.advisori.de/lumina/project-1766459980010-q6lr8s01b:v1.1.0
|
||||
|
||||
# Update with new image
|
||||
helm upgrade project-1766459980010-q6lr8s01b ./helm/lumina-app \
|
||||
--namespace lumina-apps \
|
||||
--set image.tag=v1.1.0 \
|
||||
--reuse-values
|
||||
|
||||
# Rollback if needed
|
||||
helm rollback project-1766459980010-q6lr8s01b -n lumina-apps
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
The template includes configurations for automated deployments:
|
||||
|
||||
- **Gitea Actions**: Auto-build on push to main
|
||||
- **Harbor Webhook**: Trigger K8s deployment on new image
|
||||
- **Helm Chart**: Version-controlled infrastructure
|
||||
|
||||
### Health Checks
|
||||
|
||||
The application exposes a health endpoint at `/api/health` for:
|
||||
|
||||
- Kubernetes liveness probes
|
||||
- Readiness probes
|
||||
- Load balancer health checks
|
||||
|
||||
Create `app/api/health/route.ts` if not exists:
|
||||
|
||||
```typescript
|
||||
export async function GET() {
|
||||
return Response.json({ status: 'healthy' }, { status: 200 });
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs)
|
||||
- [Supabase Documentation](https://supabase.com/docs)
|
||||
- [Spartan UI Documentation](https://www.spartan.ng/)
|
||||
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||||
- [Kubernetes Documentation](https://kubernetes.io/docs)
|
||||
- [Helm Documentation](https://helm.sh/docs)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
---
|
||||
|
||||
**Generated by Lumina** - AI-Powered Development Platform by [advisori](https://advisori.de)
|
||||
238
SECURITY.md
Normal file
238
SECURITY.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Security
|
||||
|
||||
This template includes integrated security scanning powered by Semgrep.
|
||||
|
||||
## Security Dashboard
|
||||
|
||||
Access the Security Dashboard from the **Security** tab in your project:
|
||||
|
||||
- **Security Score**: Overall security health (0-100%)
|
||||
- **Issue Breakdown**: Critical, High, Medium, and Low severity issues
|
||||
- **Scan History**: View past security scans
|
||||
- **Detailed Reports**: Line-by-line issue analysis with fix suggestions
|
||||
|
||||
## Running Security Scans
|
||||
|
||||
### Via Dashboard
|
||||
|
||||
1. Navigate to the **Security** tab
|
||||
2. Click **Run Scan** button
|
||||
3. Wait for analysis to complete
|
||||
4. Review issues and recommendations
|
||||
|
||||
### Via Lumina Skill
|
||||
|
||||
Use the built-in Semgrep security skill:
|
||||
|
||||
```
|
||||
User: "Scan the code for security issues"
|
||||
User: "Check for OWASP Top 10 vulnerabilities"
|
||||
User: "What's our security score?"
|
||||
```
|
||||
|
||||
The skill will:
|
||||
- Auto-install Semgrep if not present
|
||||
- Run comprehensive security analysis
|
||||
- Generate actionable reports
|
||||
- Update the Security Dashboard
|
||||
|
||||
### Via Command Line
|
||||
|
||||
```bash
|
||||
# Install Semgrep (if not installed) - OS-specific
|
||||
|
||||
# macOS (OSX)
|
||||
brew install semgrep
|
||||
|
||||
# Linux
|
||||
sudo apt-get update && sudo apt-get install -y semgrep
|
||||
|
||||
# Windows
|
||||
choco install semgrep -y
|
||||
|
||||
# Universal (works on all platforms)
|
||||
pip3 install semgrep
|
||||
|
||||
# Run security scan
|
||||
semgrep --config=auto --json .
|
||||
|
||||
# OWASP Top 10 scan
|
||||
semgrep --config=p/owasp-top-ten --json .
|
||||
|
||||
# Language-specific
|
||||
semgrep --config=p/typescript --json .
|
||||
semgrep --config=p/react --json .
|
||||
```
|
||||
|
||||
## Security Score Calculation
|
||||
|
||||
Your security score is calculated based on:
|
||||
|
||||
```
|
||||
Base Score: 100 points
|
||||
|
||||
Deductions:
|
||||
- Critical Issue: -10 points each
|
||||
- High Issue: -5 points each
|
||||
- Medium Issue: -2 points each
|
||||
- Low Issue: -0.5 points each
|
||||
|
||||
Final Score: max(0, Base Score - Total Deductions)
|
||||
```
|
||||
|
||||
**Score Ratings:**
|
||||
- 90-100: Excellent ✅
|
||||
- 70-89: Good 👍
|
||||
- 50-69: Fair ⚠️
|
||||
- 0-49: Poor ❌
|
||||
|
||||
## Common Security Issues
|
||||
|
||||
### Critical Issues
|
||||
|
||||
- SQL Injection vulnerabilities
|
||||
- Command Injection
|
||||
- Path Traversal
|
||||
- Hardcoded secrets/credentials
|
||||
- Insecure cryptography
|
||||
|
||||
### High Issues
|
||||
|
||||
- XSS (Cross-Site Scripting)
|
||||
- CSRF (Cross-Site Request Forgery)
|
||||
- Insecure authentication
|
||||
- Sensitive data exposure
|
||||
- Insecure deserialization
|
||||
|
||||
### Medium Issues
|
||||
|
||||
- Missing input validation
|
||||
- Weak password policies
|
||||
- Insecure session management
|
||||
- Missing security headers
|
||||
- Information disclosure
|
||||
|
||||
### Low Issues
|
||||
|
||||
- Code smells
|
||||
- Best practice violations
|
||||
- Performance issues
|
||||
- Deprecated functions
|
||||
|
||||
## Fixing Security Issues
|
||||
|
||||
### General Workflow
|
||||
|
||||
1. **Prioritize**: Fix critical and high severity issues first
|
||||
2. **Review**: Understand the vulnerability and its impact
|
||||
3. **Fix**: Apply recommended fixes or security patches
|
||||
4. **Test**: Verify the fix doesn't break functionality
|
||||
5. **Rescan**: Run a new scan to confirm the issue is resolved
|
||||
|
||||
### Using Lumina to Fix Issues
|
||||
|
||||
```
|
||||
User: "Fix the SQL injection vulnerability in user-service.ts"
|
||||
User: "Apply security patches for all critical issues"
|
||||
User: "Review and fix the XSS issue on line 45"
|
||||
```
|
||||
|
||||
Lumina will:
|
||||
- Analyze the vulnerability
|
||||
- Apply secure coding practices
|
||||
- Update the code with fixes
|
||||
- Run tests to verify
|
||||
|
||||
## Continuous Security
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Regular Scans**: Run security scans before every deployment
|
||||
2. **Pre-commit Hooks**: Add Semgrep to your git pre-commit hooks
|
||||
3. **CI/CD Integration**: Include security scans in your pipeline
|
||||
4. **Dependency Updates**: Keep dependencies up-to-date
|
||||
5. **Security Reviews**: Conduct code reviews with security focus
|
||||
|
||||
### Pre-commit Hook Example
|
||||
|
||||
Add to `.git/hooks/pre-commit`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "Running security scan..."
|
||||
semgrep --config=auto --error .
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Security issues found! Commit blocked."
|
||||
echo "Run 'semgrep --config=auto .' to see details"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
#### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Security Scan
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
semgrep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: pip3 install semgrep
|
||||
- run: semgrep --config=auto --error .
|
||||
```
|
||||
|
||||
#### GitLab CI
|
||||
|
||||
```yaml
|
||||
security_scan:
|
||||
image: returntocorp/semgrep
|
||||
script:
|
||||
- semgrep --config=auto --error .
|
||||
```
|
||||
|
||||
## Security Rules
|
||||
|
||||
Semgrep scans use the following rule sets:
|
||||
|
||||
- **auto**: Automatically curated rules for your codebase
|
||||
- **p/security-audit**: General security audit rules
|
||||
- **p/owasp-top-ten**: OWASP Top 10 vulnerabilities
|
||||
- **p/typescript**: TypeScript-specific security rules
|
||||
- **p/react**: React security best practices
|
||||
- **p/javascript**: JavaScript security patterns
|
||||
|
||||
## False Positives
|
||||
|
||||
If you encounter false positives:
|
||||
|
||||
1. Review the finding carefully
|
||||
2. Add inline comments to suppress if legitimate:
|
||||
```typescript
|
||||
// nosemgrep: rule-id
|
||||
const result = potentiallyUnsafeOperation();
|
||||
```
|
||||
3. Configure `.semgrepignore` to exclude files/patterns
|
||||
4. Report false positives to improve Semgrep rules
|
||||
|
||||
## Resources
|
||||
|
||||
- [Semgrep Documentation](https://semgrep.dev/docs/)
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [Semgrep Rule Registry](https://semgrep.dev/explore)
|
||||
- [Security Best Practices](https://cheatsheetseries.owasp.org/)
|
||||
|
||||
## Support
|
||||
|
||||
For security concerns or questions:
|
||||
- Use the Semgrep security skill in Lumina
|
||||
- Check the Security Dashboard for guidance
|
||||
- Review Semgrep documentation
|
||||
- Consult OWASP guidelines
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Security is an ongoing process, not a one-time task. Regular scans and proactive security practices keep your application safe.
|
||||
27
app/api/health/route.ts
Normal file
27
app/api/health/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Health Check Endpoint
|
||||
* Used by Kubernetes liveness and readiness probes
|
||||
*/
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Basic health check - returns 200 if app is running
|
||||
const healthData = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
};
|
||||
|
||||
return Response.json(healthData, { status: 200 });
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{
|
||||
status: 'unhealthy',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
}
|
||||
59
app/globals.css
Normal file
59
app/globals.css
Normal file
@@ -0,0 +1,59 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
22
app/layout.tsx
Normal file
22
app/layout.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Lumina',
|
||||
description: 'Full-stack Next.js application with Supabase, Spartan UI, and Tailwind CSS',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
67
app/page.tsx
Normal file
67
app/page.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center p-24">
|
||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm">
|
||||
<h1 className="text-4xl font-bold text-center mb-4">
|
||||
Welcome to Lumina
|
||||
</h1>
|
||||
<p className="text-center text-muted-foreground mb-8">
|
||||
Built with Next.js 15, Spartan UI, and Supabase
|
||||
</p>
|
||||
|
||||
<div className="grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-3 lg:text-left gap-4">
|
||||
<a
|
||||
href="https://nextjs.org/docs"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Docs{' '}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
→
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://supabase.com/docs"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Supabase{' '}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
→
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Learn about Supabase authentication and database.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://www.spartan.ng/"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Spartan UI{' '}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
→
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Explore beautiful and accessible UI components.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
50
components/ui/button.tsx
Normal file
50
components/ui/button.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
55
components/ui/card.tsx
Normal file
55
components/ui/card.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
||||
)
|
||||
);
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
)
|
||||
);
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
||||
)
|
||||
);
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
79
docker-compose.yml
Normal file
79
docker-compose.yml
Normal file
@@ -0,0 +1,79 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Next.js Application
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: lumina-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- NEXT_TELEMETRY_DISABLED=1
|
||||
env_file:
|
||||
- .env.local
|
||||
networks:
|
||||
- lumina-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.app.rule=Host(`localhost`)"
|
||||
- "traefik.http.services.app.loadbalancer.server.port=3000"
|
||||
|
||||
# PostgreSQL Database (Optional - for local development)
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: lumina-postgres
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_USER=lumina
|
||||
- POSTGRES_PASSWORD=lumina_dev_password
|
||||
- POSTGRES_DB=lumina_db
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- lumina-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U lumina"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Redis Cache (Optional - for session management)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: lumina-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
networks:
|
||||
- lumina-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
lumina-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
||||
41
helm/lumina-app/.helmignore
Normal file
41
helm/lumina-app/.helmignore
Normal file
@@ -0,0 +1,41 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
|
||||
# Helm specific
|
||||
.helmignore
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
.gitlab-ci.yml
|
||||
.travis.yml
|
||||
azure-pipelines.yml
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
CONTRIBUTING.md
|
||||
docs/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
# Testing
|
||||
test/
|
||||
tests/
|
||||
*.test.yaml
|
||||
16
helm/lumina-app/Chart.yaml
Normal file
16
helm/lumina-app/Chart.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v2
|
||||
name: lumina-app
|
||||
description: A Helm chart for Lumina Next.js applications
|
||||
type: application
|
||||
version: 1.0.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- nextjs
|
||||
- lumina
|
||||
- advisori
|
||||
maintainers:
|
||||
- name: advisori
|
||||
email: info@advisori.de
|
||||
sources:
|
||||
- https://github.com/advisori/lumina
|
||||
icon: https://advisori.de/assets/lumina-icon.svg
|
||||
81
helm/lumina-app/templates/NOTES.txt
Normal file
81
helm/lumina-app/templates/NOTES.txt
Normal file
@@ -0,0 +1,81 @@
|
||||
🚀 Lumina Application Deployed Successfully!
|
||||
|
||||
Your application has been deployed to Kubernetes with the following configuration:
|
||||
|
||||
📦 Release Name: {{ .Release.Name }}
|
||||
🏷️ Namespace: {{ .Release.Namespace }}
|
||||
📊 Replicas: {{ .Values.replicaCount }}
|
||||
🖼️ Image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
🌐 Ingress Configuration:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
🔗 https://{{ .host }}
|
||||
{{- end }}
|
||||
|
||||
{{- else }}
|
||||
|
||||
⚠️ Ingress is disabled. To access the application:
|
||||
|
||||
kubectl port-forward svc/{{ include "lumina-app.fullname" . }} 3000:{{ .Values.service.port }} -n {{ .Release.Namespace }}
|
||||
|
||||
Then open http://localhost:3000 in your browser.
|
||||
|
||||
{{- end }}
|
||||
|
||||
📋 Useful Commands:
|
||||
|
||||
# View pods
|
||||
kubectl get pods -l "app.kubernetes.io/name={{ include "lumina-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -n {{ .Release.Namespace }}
|
||||
|
||||
# View logs
|
||||
kubectl logs -f deployment/{{ include "lumina-app.fullname" . }} -n {{ .Release.Namespace }}
|
||||
|
||||
# View service
|
||||
kubectl get svc {{ include "lumina-app.fullname" . }} -n {{ .Release.Namespace }}
|
||||
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
# View autoscaler
|
||||
kubectl get hpa {{ include "lumina-app.fullname" . }} -n {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
|
||||
# Update deployment
|
||||
helm upgrade {{ .Release.Name }} . --namespace {{ .Release.Namespace }}
|
||||
|
||||
# Rollback deployment
|
||||
helm rollback {{ .Release.Name }} -n {{ .Release.Namespace }}
|
||||
|
||||
{{- if .Values.persistence.enabled }}
|
||||
|
||||
💾 Persistence is enabled at {{ .Values.persistence.mountPath }}
|
||||
|
||||
{{- end }}
|
||||
|
||||
📊 Monitoring:
|
||||
- Liveness Probe: {{ .Values.livenessProbe.httpGet.path }}
|
||||
- Readiness Probe: {{ .Values.readinessProbe.httpGet.path }}
|
||||
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
- Autoscaling: {{ .Values.autoscaling.minReplicas }}-{{ .Values.autoscaling.maxReplicas }} replicas
|
||||
- Target CPU: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}%
|
||||
- Target Memory: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}%
|
||||
{{- end }}
|
||||
|
||||
🔐 Security:
|
||||
- Non-root user: {{ .Values.securityContext.runAsUser }}
|
||||
- Read-only filesystem: {{ .Values.securityContext.readOnlyRootFilesystem }}
|
||||
|
||||
✅ Next Steps:
|
||||
1. Verify pods are running: kubectl get pods -n {{ .Release.Namespace }}
|
||||
2. Check application logs for any errors
|
||||
3. Test the health endpoint: /api/health
|
||||
4. Configure monitoring and alerting
|
||||
5. Set up backup strategy (if using persistence)
|
||||
|
||||
📚 Documentation: https://github.com/advisori/lumina
|
||||
|
||||
💬 Support: info@advisori.de
|
||||
|
||||
---
|
||||
Generated by Lumina - AI-Powered Development Platform
|
||||
60
helm/lumina-app/templates/_helpers.tpl
Normal file
60
helm/lumina-app/templates/_helpers.tpl
Normal file
@@ -0,0 +1,60 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "lumina-app.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "lumina-app.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "lumina-app.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "lumina-app.labels" -}}
|
||||
helm.sh/chart: {{ include "lumina-app.chart" . }}
|
||||
{{ include "lumina-app.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "lumina-app.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "lumina-app.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "lumina-app.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "lumina-app.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
10
helm/lumina-app/templates/configmap.yaml
Normal file
10
helm/lumina-app/templates/configmap.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
{{- if .Values.configMap.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "lumina-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- toYaml .Values.configMap.data | nindent 2 }}
|
||||
{{- end }}
|
||||
78
helm/lumina-app/templates/deployment.yaml
Normal file
78
helm/lumina-app/templates/deployment.yaml
Normal file
@@ -0,0 +1,78 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "lumina-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "lumina-app.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "lumina-app.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
env:
|
||||
{{- toYaml .Values.env | nindent 12 }}
|
||||
{{- with .Values.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- if .Values.persistence.enabled }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: {{ .Values.persistence.mountPath }}
|
||||
{{- end }}
|
||||
{{- if .Values.persistence.enabled }}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "lumina-app.fullname" . }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
32
helm/lumina-app/templates/hpa.yaml
Normal file
32
helm/lumina-app/templates/hpa.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "lumina-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "lumina-app.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
41
helm/lumina-app/templates/ingress.yaml
Normal file
41
helm/lumina-app/templates/ingress.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "lumina-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "lumina-app.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
13
helm/lumina-app/templates/pdb.yaml
Normal file
13
helm/lumina-app/templates/pdb.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.podDisruptionBudget.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "lumina-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
spec:
|
||||
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "lumina-app.selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
17
helm/lumina-app/templates/pvc.yaml
Normal file
17
helm/lumina-app/templates/pvc.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.persistence.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "lumina-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.persistence.accessMode }}
|
||||
{{- if .Values.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.persistence.storageClass }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- end }}
|
||||
11
helm/lumina-app/templates/secret.yaml
Normal file
11
helm/lumina-app/templates/secret.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
{{- if .Values.secrets.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ .Values.secrets.name | default (include "lumina-app.fullname" .) }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- toYaml .Values.secrets.data | nindent 2 }}
|
||||
{{- end }}
|
||||
19
helm/lumina-app/templates/service.yaml
Normal file
19
helm/lumina-app/templates/service.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "lumina-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "lumina-app.selectorLabels" . | nindent 4 }}
|
||||
13
helm/lumina-app/templates/serviceaccount.yaml
Normal file
13
helm/lumina-app/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "lumina-app.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "lumina-app.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
191
helm/lumina-app/values.yaml
Normal file
191
helm/lumina-app/values.yaml
Normal file
@@ -0,0 +1,191 @@
|
||||
# Default values for lumina-app
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# Number of pod replicas
|
||||
replicaCount: 2
|
||||
|
||||
# Container image configuration
|
||||
image:
|
||||
repository: harbor.advisori.de/lumina/app
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
# Image pull secrets for private registries
|
||||
imagePullSecrets:
|
||||
- name: harbor-registry-secret
|
||||
|
||||
# Override the default name
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# Service account configuration
|
||||
serviceAccount:
|
||||
create: true
|
||||
automount: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
# Pod annotations
|
||||
podAnnotations: {}
|
||||
|
||||
# Pod labels
|
||||
podLabels:
|
||||
app.kubernetes.io/component: frontend
|
||||
app.kubernetes.io/part-of: lumina
|
||||
|
||||
# Pod security context
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
fsGroup: 1001
|
||||
|
||||
# Container security context
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
|
||||
# Service configuration
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
annotations: {}
|
||||
|
||||
# Ingress configuration
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: app.advisori.de
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: lumina-app-tls
|
||||
hosts:
|
||||
- app.advisori.de
|
||||
|
||||
# Resource limits and requests
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
|
||||
# Horizontal Pod Autoscaler
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Liveness probe configuration
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
|
||||
# Readiness probe configuration
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
|
||||
# Node selector for pod scheduling
|
||||
nodeSelector: {}
|
||||
|
||||
# Tolerations for pod scheduling
|
||||
tolerations: []
|
||||
|
||||
# Affinity rules for pod scheduling
|
||||
affinity: {}
|
||||
|
||||
# Environment variables
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: NEXT_TELEMETRY_DISABLED
|
||||
value: "1"
|
||||
|
||||
# Environment variables from secrets
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: lumina-app-secrets
|
||||
|
||||
# Persistent Volume Claims (optional)
|
||||
persistence:
|
||||
enabled: false
|
||||
storageClass: ""
|
||||
accessMode: ReadWriteOnce
|
||||
size: 1Gi
|
||||
mountPath: /app/data
|
||||
|
||||
# ConfigMap for additional configuration
|
||||
configMap:
|
||||
enabled: false
|
||||
data: {}
|
||||
|
||||
# Secrets (reference existing secrets)
|
||||
secrets:
|
||||
enabled: true
|
||||
name: lumina-app-secrets
|
||||
data: {}
|
||||
# DATABASE_URL: ""
|
||||
# SUPABASE_URL: ""
|
||||
# SUPABASE_ANON_KEY: ""
|
||||
# ANTHROPIC_API_KEY: ""
|
||||
|
||||
# Network Policy
|
||||
networkPolicy:
|
||||
enabled: false
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/component: ingress-nginx
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
|
||||
# Pod Disruption Budget
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 1
|
||||
|
||||
# Service Monitor for Prometheus (optional)
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
6
lib/supabase.ts
Normal file
6
lib/supabase.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
20
next.config.js
Normal file
20
next.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '2mb',
|
||||
},
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**.supabase.co',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "project-1766459980010-q6lr8s01b",
|
||||
"version": "0.1.0",
|
||||
"description": "ads",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,md}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^15.1.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@spartan-ng/ui-core": "^0.0.1-alpha.361",
|
||||
"@supabase/supabase-js": "^2.47.10",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.460.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"typescript": "^5.7.2",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "^15.1.4",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"postcss": "^8.4.49",
|
||||
"autoprefixer": "^10.4.20"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
4042
pnpm-lock.yaml
generated
Normal file
4042
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
0
public/.gitkeep
Normal file
0
public/.gitkeep
Normal file
57
tailwind.config.ts
Normal file
57
tailwind.config.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
};
|
||||
|
||||
export default config;
|
||||
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user