Initial commit from template
This commit is contained in:
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';
|
||||
}
|
||||
Reference in New Issue
Block a user