Replace all marketing placeholder content with real MYeasyCMS content
- Logo: Replace generic Makerkit SVG with MYeasyCMS branded logo (grid icon + styled text) - Blog: Replace 3 SaaS placeholder posts with 5 real articles (Vereinsverwaltung, SEPA, Website, DSGVO, Mitglieder-Tipps) - Changelog: Replace 6 generic entries with real feature announcements (Verbandsverwaltung, Fischerei, Dateien, Kurse, Einladungen, i18n) - Documentation: Rewrite all 20 docs from Makerkit references to MYeasyCMS content - FAQ: Replace 6 generic SaaS questions with 10 real MYeasyCMS questions - Navigation: Replace Changelog link with Contact in main nav - Footer: Reorganize into Product/Company/Legal sections - Translations: Update all EN marketing strings to match real Com.BISS content
This commit is contained in:
@@ -1,392 +1,30 @@
|
||||
---
|
||||
title: "Magic Links"
|
||||
description: "Passwordless authentication with email magic links."
|
||||
title: "Einladungslinks"
|
||||
description: "Benutzer per E-Mail-Einladung in den Vereins-Account einladen."
|
||||
publishedAt: 2024-04-11
|
||||
order: 4
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
Administratoren können neue Benutzer per Einladungslink in den Vereins-Account einladen.
|
||||
|
||||
Magic links provide passwordless authentication by sending a one-time link to the user's email.
|
||||
## So funktioniert die Einladung
|
||||
|
||||
## How It Works
|
||||
1. Navigieren Sie zu **Einstellungen → Mitarbeiter → Einladen**
|
||||
2. Geben Sie die E-Mail-Adresse des neuen Benutzers ein
|
||||
3. Wählen Sie die Rolle (Administrator, Kassenwart, Kursleiter, etc.)
|
||||
4. Klicken Sie auf **Einladung senden**
|
||||
|
||||
1. User enters their email address
|
||||
2. System sends an email with a unique link
|
||||
3. User clicks the link in their email
|
||||
4. User is automatically signed in
|
||||
Der eingeladene Benutzer erhält eine E-Mail mit einem Registrierungslink. Nach der Registrierung hat er sofort Zugriff auf die Funktionen, die seiner Rolle entsprechen.
|
||||
|
||||
## Benefits
|
||||
## Einladungen verwalten
|
||||
|
||||
- **No password to remember** - Better UX
|
||||
- **More secure** - No password to steal
|
||||
- **Lower friction** - Faster sign-up process
|
||||
- **Email verification** - Confirms email ownership
|
||||
Unter **Mitglieder → Einladungen** sehen Sie alle offenen und angenommenen Einladungen:
|
||||
|
||||
## Implementation
|
||||
- **Offen** — Einladung wurde versendet, aber noch nicht angenommen
|
||||
- **Angenommen** — Benutzer hat die Einladung angenommen und sein Konto erstellt
|
||||
- **Abgelaufen** — Einladungslink ist nicht mehr gültig (kann erneut gesendet werden)
|
||||
|
||||
### Magic Link Form
|
||||
## Erneut einladen
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { sendMagicLinkAction } from '../_lib/actions';
|
||||
|
||||
export function MagicLinkForm() {
|
||||
const { register, handleSubmit, formState: { isSubmitting } } = useForm();
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const result = await sendMagicLinkAction(data);
|
||||
|
||||
if (result.success) {
|
||||
setSent(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (sent) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Check your email</h2>
|
||||
<p>We've sent you a magic link to sign in.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<label>Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register('email', { required: true })}
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Sending...' : 'Send magic link'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Server Action
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const sendMagicLinkAction = enhanceAction(
|
||||
async (data) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const origin = process.env.NEXT_PUBLIC_SITE_URL!;
|
||||
|
||||
const { error } = await client.auth.signInWithOtp({
|
||||
email: data.email,
|
||||
options: {
|
||||
emailRedirectTo: `${origin}/auth/callback`,
|
||||
shouldCreateUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Check your email for the magic link',
|
||||
};
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
email: z.string().email(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable in Supabase
|
||||
|
||||
1. Go to **Authentication** → **Providers** → **Email**
|
||||
2. Enable "Enable Email Provider"
|
||||
3. Enable "Enable Email Confirmations"
|
||||
|
||||
### Configure Email Template
|
||||
|
||||
Customize the magic link email in Supabase Dashboard:
|
||||
|
||||
1. Go to **Authentication** → **Email Templates**
|
||||
2. Select "Magic Link"
|
||||
3. Customize the template:
|
||||
|
||||
```html
|
||||
<h2>Sign in to {{ .SiteURL }}</h2>
|
||||
<p>Click the link below to sign in:</p>
|
||||
<p><a href="{{ .ConfirmationURL }}">Sign in</a></p>
|
||||
<p>This link expires in {{ .TokenExpiryHours }} hours.</p>
|
||||
```
|
||||
|
||||
## Callback Handler
|
||||
|
||||
Handle the magic link callback:
|
||||
|
||||
```typescript
|
||||
// app/auth/callback/route.ts
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const requestUrl = new URL(request.url);
|
||||
const token_hash = requestUrl.searchParams.get('token_hash');
|
||||
const type = requestUrl.searchParams.get('type');
|
||||
|
||||
if (token_hash && type === 'magiclink') {
|
||||
const cookieStore = cookies();
|
||||
const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
|
||||
|
||||
const { error } = await supabase.auth.verifyOtp({
|
||||
token_hash,
|
||||
type: 'magiclink',
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
return NextResponse.redirect(new URL('/home', request.url));
|
||||
}
|
||||
}
|
||||
|
||||
// Return error if verification failed
|
||||
return NextResponse.redirect(
|
||||
new URL('/auth/sign-in?error=invalid_link', request.url)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Custom Redirect
|
||||
|
||||
Specify where users go after clicking the link:
|
||||
|
||||
```typescript
|
||||
await client.auth.signInWithOtp({
|
||||
email: data.email,
|
||||
options: {
|
||||
emailRedirectTo: `${origin}/onboarding`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Disable Auto Sign-Up
|
||||
|
||||
Require users to sign up first:
|
||||
|
||||
```typescript
|
||||
await client.auth.signInWithOtp({
|
||||
email: data.email,
|
||||
options: {
|
||||
shouldCreateUser: false, // Don't create new users
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Token Expiry
|
||||
|
||||
Configure link expiration (default: 1 hour):
|
||||
|
||||
```sql
|
||||
-- In Supabase SQL Editor
|
||||
ALTER TABLE auth.users
|
||||
SET default_token_lifetime = '15 minutes';
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Prevent abuse by rate limiting magic link requests:
|
||||
|
||||
```typescript
|
||||
import { ratelimit } from '~/lib/rate-limit';
|
||||
|
||||
export const sendMagicLinkAction = enhanceAction(
|
||||
async (data, user, request) => {
|
||||
// Rate limit by IP
|
||||
const ip = request.headers.get('x-forwarded-for') || 'unknown';
|
||||
const { success } = await ratelimit.limit(ip);
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Too many requests. Please try again later.');
|
||||
}
|
||||
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
await client.auth.signInWithOtp({
|
||||
email: data.email,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{ schema: EmailSchema }
|
||||
);
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Link Expiration
|
||||
|
||||
Magic links should expire quickly:
|
||||
- Default: 1 hour
|
||||
- Recommended: 15-30 minutes for production
|
||||
- Shorter for sensitive actions
|
||||
|
||||
### One-Time Use
|
||||
|
||||
Links should be invalidated after use:
|
||||
|
||||
```typescript
|
||||
// Supabase handles this automatically
|
||||
// Each link can only be used once
|
||||
```
|
||||
|
||||
### Email Verification
|
||||
|
||||
Ensure emails are verified:
|
||||
|
||||
```typescript
|
||||
const { data: { user } } = await client.auth.getUser();
|
||||
|
||||
if (!user.email_confirmed_at) {
|
||||
redirect('/verify-email');
|
||||
}
|
||||
```
|
||||
|
||||
## User Experience
|
||||
|
||||
### Loading State
|
||||
|
||||
Show feedback while sending:
|
||||
|
||||
```tsx
|
||||
export function MagicLinkForm() {
|
||||
const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle');
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
setStatus('sending');
|
||||
await sendMagicLinkAction(data);
|
||||
setStatus('sent');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{status === 'idle' && <EmailForm onSubmit={onSubmit} />}
|
||||
{status === 'sending' && <SendingMessage />}
|
||||
{status === 'sent' && <CheckEmailMessage />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Resend Link
|
||||
|
||||
Allow users to request a new link:
|
||||
|
||||
```tsx
|
||||
export function ResendMagicLink({ email }: { email: string }) {
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const [countdown, setCountdown] = useState(60);
|
||||
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
const handleResend = async () => {
|
||||
await sendMagicLinkAction({ email });
|
||||
setCountdown(60);
|
||||
setCanResend(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={handleResend} disabled={!canResend}>
|
||||
{canResend ? 'Resend link' : `Resend in ${countdown}s`}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Email Deliverability
|
||||
|
||||
### SPF, DKIM, DMARC
|
||||
|
||||
Configure email authentication:
|
||||
1. Add SPF record to DNS
|
||||
2. Enable DKIM signing
|
||||
3. Set up DMARC policy
|
||||
|
||||
### Custom Email Domain
|
||||
|
||||
Use your own domain for better deliverability:
|
||||
|
||||
1. Go to **Project Settings** → **Auth**
|
||||
2. Configure custom SMTP
|
||||
3. Verify domain ownership
|
||||
|
||||
### Monitor Bounces
|
||||
|
||||
Track email delivery issues:
|
||||
|
||||
```typescript
|
||||
// Handle email bounces
|
||||
export async function handleEmailBounce(email: string) {
|
||||
await client.from('email_bounces').insert({
|
||||
email,
|
||||
bounced_at: new Date(),
|
||||
});
|
||||
|
||||
// Notify user via other channel
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Local Development
|
||||
|
||||
In development, emails go to InBucket:
|
||||
|
||||
```
|
||||
http://localhost:54324
|
||||
```
|
||||
|
||||
Check this URL to see magic link emails during testing.
|
||||
|
||||
### Test Mode
|
||||
|
||||
Create a test link without sending email:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Magic link URL:', confirmationUrl);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Clear communication** - Tell users to check spam
|
||||
2. **Short expiry** - 15-30 minutes for security
|
||||
3. **Rate limiting** - Prevent abuse
|
||||
4. **Fallback option** - Offer password auth as backup
|
||||
5. **Custom domain** - Better deliverability
|
||||
6. **Monitor delivery** - Track bounces and failures
|
||||
7. **Resend option** - Let users request new link
|
||||
8. **Mobile-friendly** - Ensure links work on mobile
|
||||
Abgelaufene oder nicht angenommene Einladungen können mit einem Klick erneut versendet werden.
|
||||
|
||||
Reference in New Issue
Block a user