Initial commit from template

This commit is contained in:
Lumina
2025-12-23 04:19:57 +01:00
commit b3d8fe8dfe
76 changed files with 10491 additions and 0 deletions

View 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

View 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;
}

View 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';
}