spaps-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +76 -0
- package/dist/chunk-GPBTYWDD.js +496 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +9 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +26 -0
- package/dist/resources/SPAPS_SURFACE_CONTRACT.md +125 -0
- package/dist/resources/glossary.md +36 -0
- package/dist/resources/llms.txt +15 -0
- package/dist/wizard/step-01-setup.md +149 -0
- package/dist/wizard/step-02-environment.md +291 -0
- package/dist/wizard/step-03-sdk-init.md +351 -0
- package/dist/wizard/step-04-email-auth.md +311 -0
- package/dist/wizard/step-05-wallet-auth.md +368 -0
- package/dist/wizard/step-06-magic-link.md +560 -0
- package/dist/wizard/step-07-payments.md +529 -0
- package/dist/wizard/step-08-whitelist.md +338 -0
- package/dist/wizard/step-09-admin.md +579 -0
- package/dist/wizard/step-10-errors.md +525 -0
- package/dist/wizard/step-11-ui-polish.md +640 -0
- package/dist/wizard/step-12-testing.md +588 -0
- package/dist/wizard/wizard.lock +67 -0
- package/package.json +66 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# SPAPS Integration Step 6/12: Magic Link Authentication
|
|
2
|
+
|
|
3
|
+
## Prerequisites Check
|
|
4
|
+
|
|
5
|
+
Before starting, confirm you have completed:
|
|
6
|
+
- ✅ Step 1-5: Full setup with email and wallet authentication
|
|
7
|
+
- ✅ TokenManager working properly
|
|
8
|
+
- ✅ Error handling implemented
|
|
9
|
+
|
|
10
|
+
## Required TodoWrite List
|
|
11
|
+
|
|
12
|
+
Create a TodoWrite with EXACTLY these items:
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
TodoWrite({
|
|
16
|
+
todos: [
|
|
17
|
+
{ content: "Add magic link tab to auth component", status: "pending", activeForm: "Adding magic link tab" },
|
|
18
|
+
{ content: "Create magic link request form", status: "pending", activeForm: "Creating magic link form" },
|
|
19
|
+
{ content: "Implement requestMagicLink method", status: "pending", activeForm: "Implementing requestMagicLink" },
|
|
20
|
+
{ content: "Add redirect_url parameter", status: "pending", activeForm: "Adding redirect URL" },
|
|
21
|
+
{ content: "Handle success/error states", status: "pending", activeForm: "Handling response states" },
|
|
22
|
+
{ content: "Create callback page for magic link", status: "pending", activeForm: "Creating callback page" },
|
|
23
|
+
{ content: "Show user feedback with toast", status: "pending", activeForm: "Adding user feedback" },
|
|
24
|
+
{ content: "Request step 7 of SPAPS integration wizard", status: "pending", activeForm: "Requesting next step" }
|
|
25
|
+
]
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Magic Link Implementation
|
|
30
|
+
|
|
31
|
+
### Critical Parameter: redirect_url
|
|
32
|
+
|
|
33
|
+
⚠️ **IMPORTANT**: The `requestMagicLink` method requires BOTH `email` and `redirect_url` parameters:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// ✅ CORRECT - Both parameters required
|
|
37
|
+
await sdk.auth.requestMagicLink({
|
|
38
|
+
email: 'user@example.com',
|
|
39
|
+
redirect_url: 'http://localhost:3000/auth/callback' // REQUIRED!
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// ❌ WRONG - Missing redirect_url will cause errors
|
|
43
|
+
await sdk.auth.requestMagicLink({
|
|
44
|
+
email: 'user@example.com' // Missing redirect_url!
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// ❌ WRONG - Wrong parameter name
|
|
48
|
+
await sdk.auth.requestMagicLink({
|
|
49
|
+
email: 'user@example.com',
|
|
50
|
+
redirectUrl: 'http://localhost:3000/auth/callback' // Wrong! Use redirect_url
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**What redirect_url does:**
|
|
55
|
+
- Where the user goes after clicking the magic link
|
|
56
|
+
- Must be a complete URL (including protocol)
|
|
57
|
+
- Usually your app's callback page
|
|
58
|
+
- Example: `http://localhost:3000/auth/callback`
|
|
59
|
+
|
|
60
|
+
### 1. Magic Link Request Form
|
|
61
|
+
|
|
62
|
+
Add to your existing auth component or create new:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// components/auth/magic-link-form.tsx
|
|
66
|
+
'use client'
|
|
67
|
+
|
|
68
|
+
import { useState } from 'react'
|
|
69
|
+
import { Button } from "@/components/ui/button"
|
|
70
|
+
import { Input } from "@/components/ui/input"
|
|
71
|
+
import { Label } from "@/components/ui/label"
|
|
72
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
73
|
+
import { toast } from "sonner"
|
|
74
|
+
import { sdk } from '@/lib/spaps'
|
|
75
|
+
import { SweetPotatoAPIError } from 'spaps-sdk'
|
|
76
|
+
import { Mail, Loader2 } from 'lucide-react'
|
|
77
|
+
|
|
78
|
+
export function MagicLinkForm() {
|
|
79
|
+
const [email, setEmail] = useState('')
|
|
80
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
81
|
+
const [isSent, setIsSent] = useState(false)
|
|
82
|
+
|
|
83
|
+
const handleMagicLink = async (e: React.FormEvent) => {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
|
|
86
|
+
if (!email) {
|
|
87
|
+
toast.error('Please enter your email address')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setIsLoading(true)
|
|
92
|
+
setIsSent(false)
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// ✅ CORRECT: Use requestMagicLink with proper parameters
|
|
96
|
+
// The redirect_url MUST be a complete URL where users go after clicking the link
|
|
97
|
+
await sdk.auth.requestMagicLink({
|
|
98
|
+
email: email.trim(),
|
|
99
|
+
redirect_url: `${window.location.origin}/auth/callback` // Complete URL required
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
setIsSent(true)
|
|
103
|
+
toast.success('Magic link sent! Check your email.')
|
|
104
|
+
|
|
105
|
+
// Optional: Clear form after success
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
setEmail('')
|
|
108
|
+
setIsSent(false)
|
|
109
|
+
}, 30000) // Reset after 30 seconds
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof SweetPotatoAPIError) {
|
|
113
|
+
switch (error.code) {
|
|
114
|
+
case 'INVALID_EMAIL':
|
|
115
|
+
toast.error('Please enter a valid email address')
|
|
116
|
+
break
|
|
117
|
+
case 'USER_NOT_FOUND':
|
|
118
|
+
toast.error('No account found with this email')
|
|
119
|
+
break
|
|
120
|
+
case 'RATE_LIMITED':
|
|
121
|
+
toast.error('Too many requests. Please wait before trying again.')
|
|
122
|
+
break
|
|
123
|
+
case 'EMAIL_PROVIDER_ERROR':
|
|
124
|
+
toast.error('Failed to send email. Please try again.')
|
|
125
|
+
break
|
|
126
|
+
default:
|
|
127
|
+
toast.error(error.message || 'Failed to send magic link')
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
toast.error('An unexpected error occurred')
|
|
131
|
+
}
|
|
132
|
+
console.error('Magic link error:', error)
|
|
133
|
+
} finally {
|
|
134
|
+
setIsLoading(false)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const handleResend = () => {
|
|
139
|
+
setIsSent(false)
|
|
140
|
+
handleMagicLink(new Event('submit') as any)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Card className="w-[400px]">
|
|
145
|
+
<CardHeader>
|
|
146
|
+
<CardTitle>Magic Link Sign In</CardTitle>
|
|
147
|
+
<CardDescription>
|
|
148
|
+
We'll email you a secure link to sign in instantly
|
|
149
|
+
</CardDescription>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<CardContent>
|
|
152
|
+
<form onSubmit={handleMagicLink} className="space-y-4">
|
|
153
|
+
{!isSent ? (
|
|
154
|
+
<>
|
|
155
|
+
<div className="space-y-2">
|
|
156
|
+
<Label htmlFor="magic-email">Email Address</Label>
|
|
157
|
+
<Input
|
|
158
|
+
id="magic-email"
|
|
159
|
+
type="email"
|
|
160
|
+
value={email}
|
|
161
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
162
|
+
placeholder="your@email.com"
|
|
163
|
+
required
|
|
164
|
+
disabled={isLoading}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<Button
|
|
169
|
+
type="submit"
|
|
170
|
+
className="w-full"
|
|
171
|
+
disabled={isLoading}
|
|
172
|
+
>
|
|
173
|
+
{isLoading ? (
|
|
174
|
+
<>
|
|
175
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
176
|
+
Sending...
|
|
177
|
+
</>
|
|
178
|
+
) : (
|
|
179
|
+
<>
|
|
180
|
+
<Mail className="mr-2 h-4 w-4" />
|
|
181
|
+
Send Magic Link
|
|
182
|
+
</>
|
|
183
|
+
)}
|
|
184
|
+
</Button>
|
|
185
|
+
</>
|
|
186
|
+
) : (
|
|
187
|
+
<div className="space-y-4">
|
|
188
|
+
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
|
189
|
+
<p className="text-sm text-green-800 dark:text-green-200">
|
|
190
|
+
✅ Magic link sent to <strong>{email}</strong>
|
|
191
|
+
</p>
|
|
192
|
+
<p className="text-xs text-green-600 dark:text-green-400 mt-2">
|
|
193
|
+
Check your email and click the link to sign in.
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<Button
|
|
198
|
+
variant="outline"
|
|
199
|
+
className="w-full"
|
|
200
|
+
onClick={handleResend}
|
|
201
|
+
>
|
|
202
|
+
Resend Magic Link
|
|
203
|
+
</Button>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
<p className="text-xs text-muted-foreground text-center">
|
|
208
|
+
{isSent
|
|
209
|
+
? "Didn't receive it? Check your spam folder or resend."
|
|
210
|
+
: "No password needed. Just click the link in your email."}
|
|
211
|
+
</p>
|
|
212
|
+
</form>
|
|
213
|
+
</CardContent>
|
|
214
|
+
</Card>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 2. Magic Link Callback Page
|
|
220
|
+
|
|
221
|
+
Create a page to handle the magic link callback:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// app/auth/callback/page.tsx
|
|
225
|
+
'use client'
|
|
226
|
+
|
|
227
|
+
import { useEffect, useState } from 'react'
|
|
228
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
229
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
230
|
+
import { toast } from "sonner"
|
|
231
|
+
import { sdk, TokenManager } from '@/lib/spaps'
|
|
232
|
+
import { Loader2 } from 'lucide-react'
|
|
233
|
+
|
|
234
|
+
export default function MagicLinkCallback() {
|
|
235
|
+
const router = useRouter()
|
|
236
|
+
const searchParams = useSearchParams()
|
|
237
|
+
const [status, setStatus] = useState<'verifying' | 'success' | 'error'>('verifying')
|
|
238
|
+
const [error, setError] = useState<string>('')
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
const verifyMagicLink = async () => {
|
|
242
|
+
const token = searchParams.get('token')
|
|
243
|
+
const email = searchParams.get('email')
|
|
244
|
+
|
|
245
|
+
if (!token) {
|
|
246
|
+
setStatus('error')
|
|
247
|
+
setError('Invalid magic link. Token missing.')
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// Verify the magic link token
|
|
253
|
+
const authResponse = await sdk.auth.verifyMagicLink({
|
|
254
|
+
token,
|
|
255
|
+
email: email || undefined
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// Store tokens
|
|
259
|
+
TokenManager.storeTokens(authResponse)
|
|
260
|
+
|
|
261
|
+
// Setup auto-refresh
|
|
262
|
+
TokenManager.autoRefreshToken(sdk)
|
|
263
|
+
|
|
264
|
+
setStatus('success')
|
|
265
|
+
toast.success('Successfully signed in!')
|
|
266
|
+
|
|
267
|
+
// Redirect after short delay
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
router.push('/dashboard') // Or wherever you want
|
|
270
|
+
}, 2000)
|
|
271
|
+
|
|
272
|
+
} catch (error: any) {
|
|
273
|
+
setStatus('error')
|
|
274
|
+
setError(error.message || 'Failed to verify magic link')
|
|
275
|
+
toast.error('Magic link verification failed')
|
|
276
|
+
console.error('Magic link verification error:', error)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
verifyMagicLink()
|
|
281
|
+
}, [searchParams, router])
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div className="min-h-screen flex items-center justify-center p-4">
|
|
285
|
+
<Card className="w-[400px]">
|
|
286
|
+
<CardHeader>
|
|
287
|
+
<CardTitle>
|
|
288
|
+
{status === 'verifying' && 'Verifying Magic Link...'}
|
|
289
|
+
{status === 'success' && 'Success!'}
|
|
290
|
+
{status === 'error' && 'Verification Failed'}
|
|
291
|
+
</CardTitle>
|
|
292
|
+
<CardDescription>
|
|
293
|
+
{status === 'verifying' && 'Please wait while we sign you in'}
|
|
294
|
+
{status === 'success' && 'You have been successfully authenticated'}
|
|
295
|
+
{status === 'error' && 'There was a problem with your magic link'}
|
|
296
|
+
</CardDescription>
|
|
297
|
+
</CardHeader>
|
|
298
|
+
<CardContent>
|
|
299
|
+
{status === 'verifying' && (
|
|
300
|
+
<div className="flex justify-center py-8">
|
|
301
|
+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
{status === 'success' && (
|
|
306
|
+
<div className="text-center py-8">
|
|
307
|
+
<div className="text-green-600 text-5xl mb-4">✓</div>
|
|
308
|
+
<p className="text-sm text-muted-foreground">
|
|
309
|
+
Redirecting to your dashboard...
|
|
310
|
+
</p>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{status === 'error' && (
|
|
315
|
+
<div className="space-y-4">
|
|
316
|
+
<div className="text-center py-4">
|
|
317
|
+
<div className="text-red-600 text-5xl mb-4">✗</div>
|
|
318
|
+
<p className="text-sm text-red-600">
|
|
319
|
+
{error}
|
|
320
|
+
</p>
|
|
321
|
+
</div>
|
|
322
|
+
<div className="space-y-2">
|
|
323
|
+
<Button
|
|
324
|
+
variant="outline"
|
|
325
|
+
className="w-full"
|
|
326
|
+
onClick={() => router.push('/auth')}
|
|
327
|
+
>
|
|
328
|
+
Back to Sign In
|
|
329
|
+
</Button>
|
|
330
|
+
<Button
|
|
331
|
+
className="w-full"
|
|
332
|
+
onClick={() => router.push('/auth?tab=magic')}
|
|
333
|
+
>
|
|
334
|
+
Request New Magic Link
|
|
335
|
+
</Button>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
</CardContent>
|
|
340
|
+
</Card>
|
|
341
|
+
</div>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### 3. Integration with Tabs
|
|
347
|
+
|
|
348
|
+
Add magic link to your main auth component:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// components/auth/auth-tabs.tsx
|
|
352
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
353
|
+
import { EmailAuthForm } from './email-auth-form'
|
|
354
|
+
import { WalletAuthForm } from './wallet-auth-form'
|
|
355
|
+
import { MagicLinkForm } from './magic-link-form'
|
|
356
|
+
|
|
357
|
+
export function AuthTabs() {
|
|
358
|
+
return (
|
|
359
|
+
<Tabs defaultValue="email" className="w-[400px]">
|
|
360
|
+
<TabsList className="grid w-full grid-cols-3">
|
|
361
|
+
<TabsTrigger value="email">Email</TabsTrigger>
|
|
362
|
+
<TabsTrigger value="wallet">Wallet</TabsTrigger>
|
|
363
|
+
<TabsTrigger value="magic">Magic Link</TabsTrigger>
|
|
364
|
+
</TabsList>
|
|
365
|
+
|
|
366
|
+
<TabsContent value="email">
|
|
367
|
+
<EmailAuthForm />
|
|
368
|
+
</TabsContent>
|
|
369
|
+
|
|
370
|
+
<TabsContent value="wallet">
|
|
371
|
+
<WalletAuthForm />
|
|
372
|
+
</TabsContent>
|
|
373
|
+
|
|
374
|
+
<TabsContent value="magic">
|
|
375
|
+
<MagicLinkForm />
|
|
376
|
+
</TabsContent>
|
|
377
|
+
</Tabs>
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Method Signatures
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// Request magic link
|
|
386
|
+
sdk.auth.requestMagicLink({
|
|
387
|
+
email: string,
|
|
388
|
+
redirect_url?: string // Where to redirect after verification
|
|
389
|
+
}): Promise<void>
|
|
390
|
+
|
|
391
|
+
// Verify magic link (on callback page)
|
|
392
|
+
sdk.auth.verifyMagicLink({
|
|
393
|
+
token: string,
|
|
394
|
+
email?: string // Optional, for extra validation
|
|
395
|
+
}): Promise<AuthResponse>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Complete Working Example
|
|
399
|
+
|
|
400
|
+
Here's the complete flow with all required pieces:
|
|
401
|
+
|
|
402
|
+
### Step 1: Request Magic Link
|
|
403
|
+
```typescript
|
|
404
|
+
// In your component
|
|
405
|
+
const handleMagicLink = async () => {
|
|
406
|
+
try {
|
|
407
|
+
// ✅ CORRECT: Both email and redirect_url required
|
|
408
|
+
await sdk.auth.requestMagicLink({
|
|
409
|
+
email: 'test@example.com',
|
|
410
|
+
redirect_url: 'http://localhost:3000/auth/callback' // MUST be complete URL
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
console.log('Magic link sent successfully!')
|
|
414
|
+
// Show success UI to user
|
|
415
|
+
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error('Failed to send magic link:', error)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Step 2: User Clicks Link in Email
|
|
423
|
+
The magic link will look like:
|
|
424
|
+
```
|
|
425
|
+
http://localhost:3000/auth/callback?token=abc123&email=test@example.com
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Step 3: Callback Page Handles Verification
|
|
429
|
+
```typescript
|
|
430
|
+
// app/auth/callback/page.tsx
|
|
431
|
+
const token = searchParams.get('token')
|
|
432
|
+
const email = searchParams.get('email')
|
|
433
|
+
|
|
434
|
+
// ✅ CORRECT: Verify the magic link
|
|
435
|
+
const authResponse = await sdk.auth.verifyMagicLink({
|
|
436
|
+
token: token!,
|
|
437
|
+
email: email || undefined // Optional but recommended
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
// Store tokens and redirect
|
|
441
|
+
TokenManager.storeTokens(authResponse)
|
|
442
|
+
router.push('/dashboard')
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Step 4: URLs Must Match
|
|
446
|
+
⚠️ **CRITICAL**: The redirect_url in your request MUST match your callback page route:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// If your callback page is at app/auth/callback/page.tsx
|
|
450
|
+
// Then redirect_url MUST be:
|
|
451
|
+
redirect_url: `${window.location.origin}/auth/callback`
|
|
452
|
+
|
|
453
|
+
// NOT these:
|
|
454
|
+
redirect_url: '/auth/callback' // Missing protocol/domain
|
|
455
|
+
redirect_url: 'http://localhost:3000/callback' // Wrong path
|
|
456
|
+
redirect_url: `${window.location.origin}/login` // Wrong path
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Validation Checklist
|
|
460
|
+
|
|
461
|
+
✅ **Implementation**:
|
|
462
|
+
- `requestMagicLink` method called correctly
|
|
463
|
+
- `redirect_url` parameter included
|
|
464
|
+
- Success state shows confirmation
|
|
465
|
+
- Callback page handles verification
|
|
466
|
+
|
|
467
|
+
✅ **User Experience**:
|
|
468
|
+
- Clear success message after sending
|
|
469
|
+
- Loading states during request
|
|
470
|
+
- Resend option available
|
|
471
|
+
- Helpful error messages
|
|
472
|
+
|
|
473
|
+
✅ **Error Handling**:
|
|
474
|
+
- Invalid email format
|
|
475
|
+
- User not found
|
|
476
|
+
- Rate limiting
|
|
477
|
+
- Email provider errors
|
|
478
|
+
|
|
479
|
+
✅ **Testing**:
|
|
480
|
+
- Can request magic link
|
|
481
|
+
- Email shows as sent
|
|
482
|
+
- Callback page loads
|
|
483
|
+
- Tokens stored after verification
|
|
484
|
+
|
|
485
|
+
## Common Mistakes to Avoid
|
|
486
|
+
|
|
487
|
+
❌ **Forgetting redirect URL**:
|
|
488
|
+
```typescript
|
|
489
|
+
// WRONG - Missing redirect_url
|
|
490
|
+
await sdk.auth.requestMagicLink({ email })
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
❌ **Not handling sent state**:
|
|
494
|
+
```typescript
|
|
495
|
+
// WRONG - No feedback that email was sent
|
|
496
|
+
await sdk.auth.requestMagicLink(...)
|
|
497
|
+
// User doesn't know if it worked
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
❌ **Missing callback page**:
|
|
501
|
+
```typescript
|
|
502
|
+
// WRONG - Redirect URL points to non-existent page
|
|
503
|
+
redirect_url: '/auth/callback' // But page doesn't exist!
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## Testing Magic Links
|
|
507
|
+
|
|
508
|
+
### Local Development
|
|
509
|
+
|
|
510
|
+
In local mode, SPAPS will:
|
|
511
|
+
1. Log the magic link to console (no real email sent)
|
|
512
|
+
2. You can copy the link from terminal
|
|
513
|
+
3. Open in browser to test callback
|
|
514
|
+
|
|
515
|
+
### Test Flow
|
|
516
|
+
|
|
517
|
+
1. Enter email: `test@example.com`
|
|
518
|
+
2. Click "Send Magic Link"
|
|
519
|
+
3. Check terminal for link (local mode)
|
|
520
|
+
4. Open link in browser
|
|
521
|
+
5. Should authenticate and redirect
|
|
522
|
+
|
|
523
|
+
### Email Templates
|
|
524
|
+
|
|
525
|
+
SPAPS uses default email templates. In production, you can customize:
|
|
526
|
+
- Subject line
|
|
527
|
+
- Email body
|
|
528
|
+
- Branding
|
|
529
|
+
- Link expiration time
|
|
530
|
+
|
|
531
|
+
## Security Considerations
|
|
532
|
+
|
|
533
|
+
⚠️ **Important Security Notes**:
|
|
534
|
+
- Magic links expire after 15 minutes
|
|
535
|
+
- Single use only (can't reuse link)
|
|
536
|
+
- Rate limited to prevent abuse
|
|
537
|
+
- Verify token server-side
|
|
538
|
+
- Use HTTPS in production
|
|
539
|
+
|
|
540
|
+
## Next Step
|
|
541
|
+
|
|
542
|
+
When ALL todos are ✅ complete and magic links work:
|
|
543
|
+
|
|
544
|
+
```javascript
|
|
545
|
+
mcp__product-manager__get_agent_instructions({
|
|
546
|
+
category: "spaps-integration",
|
|
547
|
+
project: "spaps-demo",
|
|
548
|
+
step: 7
|
|
549
|
+
})
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Summary
|
|
553
|
+
|
|
554
|
+
Magic link authentication provides:
|
|
555
|
+
- **Passwordless login** - No passwords to remember
|
|
556
|
+
- **Better security** - No password to steal
|
|
557
|
+
- **Simple UX** - Just click the email link
|
|
558
|
+
- **Mobile friendly** - Works great on phones
|
|
559
|
+
|
|
560
|
+
Remember to test the full flow from request to callback!
|