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.
@@ -0,0 +1,579 @@
1
+ # SPAPS Integration Step 9/12: Admin Operations
2
+
3
+ ## Prerequisites Check
4
+
5
+ Before starting, confirm you have completed:
6
+ - ✅ Step 1-8: Full authentication, payments, and whitelist
7
+ - ✅ Admin credentials configured in environment
8
+ - ✅ Understanding of JWT authentication
9
+
10
+ ## Required TodoWrite List
11
+
12
+ Create a TodoWrite with EXACTLY these items:
13
+
14
+ ```javascript
15
+ TodoWrite({
16
+ todos: [
17
+ { content: "Create admin panel component", status: "pending", activeForm: "Creating admin panel" },
18
+ { content: "Implement admin authentication first", status: "pending", activeForm: "Implementing admin auth" },
19
+ { content: "Use JWT for all admin API calls", status: "pending", activeForm: "Using JWT for API calls" },
20
+ { content: "Implement product sync endpoint", status: "pending", activeForm: "Implementing product sync" },
21
+ { content: "Add user management operations", status: "pending", activeForm: "Adding user management" },
22
+ { content: "Create admin statistics view", status: "pending", activeForm: "Creating statistics view" },
23
+ { content: "Add proper error handling for 403", status: "pending", activeForm: "Adding authorization errors" },
24
+ { content: "Request step 10 of SPAPS integration wizard", status: "pending", activeForm: "Requesting next step" }
25
+ ]
26
+ })
27
+ ```
28
+
29
+ ## Admin Authentication Pattern
30
+
31
+ ⚠️ **CRITICAL**: Admin operations are NOT in the SDK. You must:
32
+ 1. Authenticate as admin using `signInWithPassword`
33
+ 2. Get the JWT token from the response
34
+ 3. Use that token for all admin API calls
35
+
36
+ ## Implementation Guide
37
+
38
+ ### 1. Admin Service
39
+
40
+ Create a service for admin operations:
41
+
42
+ ```typescript
43
+ // lib/admin-service.ts
44
+ import { sdk } from './spaps'
45
+
46
+ interface AdminAuth {
47
+ access_token: string
48
+ refresh_token: string
49
+ expires_in: number
50
+ }
51
+
52
+ class AdminService {
53
+ private adminToken: string | null = null
54
+ private tokenExpiry: number = 0
55
+
56
+ /**
57
+ * Authenticate as admin and store token
58
+ */
59
+ async authenticate(): Promise<void> {
60
+ // Check if we have a valid token
61
+ if (this.adminToken && Date.now() < this.tokenExpiry) {
62
+ return
63
+ }
64
+
65
+ const email = process.env.NEXT_PUBLIC_ADMIN_EMAIL
66
+ const password = process.env.NEXT_PUBLIC_ADMIN_PASSWORD
67
+
68
+ if (!email || !password) {
69
+ throw new Error('Admin credentials not configured')
70
+ }
71
+
72
+ try {
73
+ // Authenticate using SDK
74
+ const auth = await sdk.auth.signInWithPassword({
75
+ email,
76
+ password
77
+ })
78
+
79
+ this.adminToken = auth.access_token
80
+ this.tokenExpiry = Date.now() + (auth.expires_in * 1000)
81
+ } catch (error) {
82
+ console.error('Admin authentication failed:', error)
83
+ throw new Error('Failed to authenticate as admin')
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Get admin headers for API calls
89
+ */
90
+ async getHeaders(): Promise<HeadersInit> {
91
+ await this.authenticate()
92
+
93
+ return {
94
+ 'Authorization': `Bearer ${this.adminToken}`,
95
+ 'Content-Type': 'application/json',
96
+ 'X-API-Key': process.env.SPAPS_API_KEY || 'test_key_local_dev_only'
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Make authenticated admin API call
102
+ */
103
+ async apiCall<T>(
104
+ endpoint: string,
105
+ options: RequestInit = {}
106
+ ): Promise<T> {
107
+ const headers = await this.getHeaders()
108
+ const apiUrl = process.env.NEXT_PUBLIC_SPAPS_API_URL || 'http://localhost:3301'
109
+
110
+ const response = await fetch(`${apiUrl}${endpoint}`, {
111
+ ...options,
112
+ headers: {
113
+ ...headers,
114
+ ...(options.headers || {})
115
+ }
116
+ })
117
+
118
+ if (response.status === 403) {
119
+ throw new Error('Admin privileges required')
120
+ }
121
+
122
+ if (!response.ok) {
123
+ const error = await response.json().catch(() => ({}))
124
+ throw new Error(error.message || `API call failed: ${response.status}`)
125
+ }
126
+
127
+ return response.json()
128
+ }
129
+
130
+ /**
131
+ * Sync products from Stripe
132
+ */
133
+ async syncProducts(): Promise<any> {
134
+ return this.apiCall('/api/v1/admin/products/sync', {
135
+ method: 'POST'
136
+ })
137
+ }
138
+
139
+ /**
140
+ * Get admin statistics
141
+ */
142
+ async getStats(): Promise<any> {
143
+ return this.apiCall('/api/v1/admin/stats')
144
+ }
145
+
146
+ /**
147
+ * List all users
148
+ */
149
+ async listUsers(params?: {
150
+ limit?: number
151
+ offset?: number
152
+ search?: string
153
+ }): Promise<any> {
154
+ const queryParams = new URLSearchParams()
155
+ if (params?.limit) queryParams.append('limit', params.limit.toString())
156
+ if (params?.offset) queryParams.append('offset', params.offset.toString())
157
+ if (params?.search) queryParams.append('search', params.search)
158
+
159
+ return this.apiCall(`/api/v1/admin/users?${queryParams}`)
160
+ }
161
+
162
+ /**
163
+ * Update user status
164
+ */
165
+ async updateUserStatus(
166
+ userId: string,
167
+ status: 'active' | 'suspended' | 'deleted'
168
+ ): Promise<any> {
169
+ return this.apiCall(`/api/v1/admin/users/${userId}/status`, {
170
+ method: 'PUT',
171
+ body: JSON.stringify({ status })
172
+ })
173
+ }
174
+
175
+ /**
176
+ * Get audit logs
177
+ */
178
+ async getAuditLogs(params?: {
179
+ limit?: number
180
+ offset?: number
181
+ userId?: string
182
+ action?: string
183
+ }): Promise<any> {
184
+ const queryParams = new URLSearchParams()
185
+ if (params?.limit) queryParams.append('limit', params.limit.toString())
186
+ if (params?.offset) queryParams.append('offset', params.offset.toString())
187
+ if (params?.userId) queryParams.append('user_id', params.userId)
188
+ if (params?.action) queryParams.append('action', params.action)
189
+
190
+ return this.apiCall(`/api/v1/admin/audit-logs?${queryParams}`)
191
+ }
192
+ }
193
+
194
+ export const adminService = new AdminService()
195
+ ```
196
+
197
+ ### 2. Admin Panel Component
198
+
199
+ ```typescript
200
+ // components/admin/admin-panel.tsx
201
+ 'use client'
202
+
203
+ import { useState, useEffect } from 'react'
204
+ import { Button } from "@/components/ui/button"
205
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
206
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
207
+ import { Badge } from "@/components/ui/badge"
208
+ import { toast } from "sonner"
209
+ import { adminService } from '@/lib/admin-service'
210
+ import {
211
+ Users,
212
+ Package,
213
+ Activity,
214
+ Shield,
215
+ RefreshCw,
216
+ Loader2,
217
+ AlertTriangle
218
+ } from 'lucide-react'
219
+
220
+ interface Stats {
221
+ users: {
222
+ total: number
223
+ active: number
224
+ new_today: number
225
+ }
226
+ payments: {
227
+ revenue_month: number
228
+ active_subscriptions: number
229
+ failed_payments: number
230
+ }
231
+ system: {
232
+ api_calls_today: number
233
+ error_rate: number
234
+ uptime: string
235
+ }
236
+ }
237
+
238
+ export function AdminPanel() {
239
+ const [isLoading, setIsLoading] = useState(false)
240
+ const [stats, setStats] = useState<Stats | null>(null)
241
+ const [users, setUsers] = useState<any[]>([])
242
+ const [isAdmin, setIsAdmin] = useState<boolean | null>(null)
243
+
244
+ useEffect(() => {
245
+ checkAdminAccess()
246
+ }, [])
247
+
248
+ const checkAdminAccess = async () => {
249
+ try {
250
+ await adminService.authenticate()
251
+ setIsAdmin(true)
252
+ loadStats()
253
+ } catch (error) {
254
+ setIsAdmin(false)
255
+ toast.error('Admin access denied')
256
+ }
257
+ }
258
+
259
+ const loadStats = async () => {
260
+ try {
261
+ setIsLoading(true)
262
+ const data = await adminService.getStats()
263
+ setStats(data)
264
+ } catch (error: any) {
265
+ toast.error(`Failed to load stats: ${error.message}`)
266
+ } finally {
267
+ setIsLoading(false)
268
+ }
269
+ }
270
+
271
+ const syncProducts = async () => {
272
+ setIsLoading(true)
273
+ try {
274
+ const result = await adminService.syncProducts()
275
+ toast.success(`Synced ${result.synced_count} products from Stripe`)
276
+ } catch (error: any) {
277
+ if (error.message.includes('403')) {
278
+ toast.error('Admin privileges required')
279
+ } else {
280
+ toast.error(`Sync failed: ${error.message}`)
281
+ }
282
+ } finally {
283
+ setIsLoading(false)
284
+ }
285
+ }
286
+
287
+ const loadUsers = async () => {
288
+ setIsLoading(true)
289
+ try {
290
+ const data = await adminService.listUsers({ limit: 10 })
291
+ setUsers(data.users || [])
292
+ } catch (error: any) {
293
+ toast.error(`Failed to load users: ${error.message}`)
294
+ } finally {
295
+ setIsLoading(false)
296
+ }
297
+ }
298
+
299
+ if (isAdmin === false) {
300
+ return (
301
+ <Card className="w-full max-w-2xl">
302
+ <CardHeader>
303
+ <CardTitle className="flex items-center gap-2 text-red-600">
304
+ <AlertTriangle className="h-5 w-5" />
305
+ Admin Access Required
306
+ </CardTitle>
307
+ <CardDescription>
308
+ You must be authenticated as an admin to access this panel
309
+ </CardDescription>
310
+ </CardHeader>
311
+ <CardContent>
312
+ <p className="text-sm text-muted-foreground">
313
+ Please ensure you have admin credentials configured and try again.
314
+ </p>
315
+ </CardContent>
316
+ </Card>
317
+ )
318
+ }
319
+
320
+ if (isAdmin === null) {
321
+ return (
322
+ <Card className="w-full max-w-2xl">
323
+ <CardContent className="flex justify-center py-8">
324
+ <Loader2 className="h-8 w-8 animate-spin" />
325
+ </CardContent>
326
+ </Card>
327
+ )
328
+ }
329
+
330
+ return (
331
+ <div className="w-full max-w-4xl space-y-6">
332
+ {/* Stats Overview */}
333
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
334
+ <Card>
335
+ <CardHeader className="pb-2">
336
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
337
+ <Users className="h-4 w-4" />
338
+ Total Users
339
+ </CardTitle>
340
+ </CardHeader>
341
+ <CardContent>
342
+ <p className="text-2xl font-bold">{stats?.users.total || 0}</p>
343
+ <p className="text-xs text-muted-foreground">
344
+ +{stats?.users.new_today || 0} today
345
+ </p>
346
+ </CardContent>
347
+ </Card>
348
+
349
+ <Card>
350
+ <CardHeader className="pb-2">
351
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
352
+ <Package className="h-4 w-4" />
353
+ Active Subscriptions
354
+ </CardTitle>
355
+ </CardHeader>
356
+ <CardContent>
357
+ <p className="text-2xl font-bold">
358
+ {stats?.payments.active_subscriptions || 0}
359
+ </p>
360
+ <p className="text-xs text-muted-foreground">
361
+ ${(stats?.payments.revenue_month || 0) / 100}/mo
362
+ </p>
363
+ </CardContent>
364
+ </Card>
365
+
366
+ <Card>
367
+ <CardHeader className="pb-2">
368
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
369
+ <Activity className="h-4 w-4" />
370
+ API Calls Today
371
+ </CardTitle>
372
+ </CardHeader>
373
+ <CardContent>
374
+ <p className="text-2xl font-bold">
375
+ {stats?.system.api_calls_today || 0}
376
+ </p>
377
+ <p className="text-xs text-muted-foreground">
378
+ {stats?.system.error_rate || 0}% error rate
379
+ </p>
380
+ </CardContent>
381
+ </Card>
382
+ </div>
383
+
384
+ {/* Admin Actions */}
385
+ <Card>
386
+ <CardHeader>
387
+ <CardTitle className="flex items-center gap-2">
388
+ <Shield className="h-5 w-5" />
389
+ Admin Actions
390
+ </CardTitle>
391
+ <CardDescription>
392
+ Manage system operations and sync data
393
+ </CardDescription>
394
+ </CardHeader>
395
+ <CardContent>
396
+ <Tabs defaultValue="products">
397
+ <TabsList>
398
+ <TabsTrigger value="products">Products</TabsTrigger>
399
+ <TabsTrigger value="users">Users</TabsTrigger>
400
+ <TabsTrigger value="system">System</TabsTrigger>
401
+ </TabsList>
402
+
403
+ <TabsContent value="products" className="space-y-4">
404
+ <div>
405
+ <h4 className="text-sm font-medium mb-2">Stripe Sync</h4>
406
+ <p className="text-xs text-muted-foreground mb-4">
407
+ Sync products and prices from your Stripe account
408
+ </p>
409
+ <Button
410
+ onClick={syncProducts}
411
+ disabled={isLoading}
412
+ >
413
+ {isLoading ? (
414
+ <>
415
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
416
+ Syncing...
417
+ </>
418
+ ) : (
419
+ <>
420
+ <RefreshCw className="mr-2 h-4 w-4" />
421
+ Sync Products
422
+ </>
423
+ )}
424
+ </Button>
425
+ </div>
426
+ </TabsContent>
427
+
428
+ <TabsContent value="users" className="space-y-4">
429
+ <div>
430
+ <h4 className="text-sm font-medium mb-2">User Management</h4>
431
+ <Button
432
+ variant="outline"
433
+ onClick={loadUsers}
434
+ disabled={isLoading}
435
+ >
436
+ Load Users
437
+ </Button>
438
+ {users.length > 0 && (
439
+ <div className="mt-4 space-y-2">
440
+ {users.map((user) => (
441
+ <div key={user.id} className="flex items-center justify-between p-2 border rounded">
442
+ <div>
443
+ <p className="text-sm font-medium">{user.email}</p>
444
+ <p className="text-xs text-muted-foreground">
445
+ ID: {user.id}
446
+ </p>
447
+ </div>
448
+ <Badge variant={user.status === 'active' ? 'default' : 'secondary'}>
449
+ {user.status}
450
+ </Badge>
451
+ </div>
452
+ ))}
453
+ </div>
454
+ )}
455
+ </div>
456
+ </TabsContent>
457
+
458
+ <TabsContent value="system" className="space-y-4">
459
+ <div>
460
+ <h4 className="text-sm font-medium mb-2">System Status</h4>
461
+ <div className="space-y-2">
462
+ <div className="flex justify-between text-sm">
463
+ <span>Uptime</span>
464
+ <span className="font-mono">{stats?.system.uptime || 'N/A'}</span>
465
+ </div>
466
+ <div className="flex justify-between text-sm">
467
+ <span>Error Rate</span>
468
+ <span className="font-mono">{stats?.system.error_rate || 0}%</span>
469
+ </div>
470
+ </div>
471
+ </div>
472
+ </TabsContent>
473
+ </Tabs>
474
+ </CardContent>
475
+ </Card>
476
+ </div>
477
+ )
478
+ }
479
+ ```
480
+
481
+ ## Available Admin Endpoints
482
+
483
+ These require admin JWT authentication:
484
+
485
+ ```typescript
486
+ // Products
487
+ POST /api/v1/admin/products/sync
488
+ PUT /api/v1/admin/products/:id
489
+ DELETE /api/v1/admin/products/:id
490
+
491
+ // Users
492
+ GET /api/v1/admin/users
493
+ PUT /api/v1/admin/users/:id/status
494
+ DELETE /api/v1/admin/users/:id
495
+
496
+ // Statistics
497
+ GET /api/v1/admin/stats
498
+ GET /api/v1/admin/audit-logs
499
+
500
+ // System
501
+ POST /api/v1/admin/cache/clear
502
+ GET /api/v1/admin/health
503
+ ```
504
+
505
+ ## Validation Checklist
506
+
507
+ ✅ **Authentication**:
508
+ - Admin credentials in environment
509
+ - JWT token obtained via signInWithPassword
510
+ - Token used in Authorization header
511
+
512
+ ✅ **API Calls**:
513
+ - All admin endpoints use JWT
514
+ - Handle 403 Forbidden errors
515
+ - Show appropriate error messages
516
+
517
+ ✅ **Implementation**:
518
+ - Admin service handles authentication
519
+ - Panel shows statistics
520
+ - Product sync works
521
+ - User management available
522
+
523
+ ## Common Mistakes to Avoid
524
+
525
+ ❌ **Looking for admin methods in SDK**:
526
+ ```typescript
527
+ // WRONG - These don't exist
528
+ sdk.admin.syncProducts()
529
+ sdk.admin.getStats()
530
+ ```
531
+
532
+ ❌ **Using regular user token**:
533
+ ```typescript
534
+ // WRONG - Must be admin token
535
+ const token = TokenManager.getAccessToken() // User token won't work
536
+ ```
537
+
538
+ ❌ **Not handling 403 errors**:
539
+ ```typescript
540
+ // WRONG - No error handling
541
+ const response = await fetch('/api/v1/admin/stats')
542
+ // Will fail with 403 if not admin
543
+ ```
544
+
545
+ ## Testing Admin Operations
546
+
547
+ 1. **Check Admin Auth**:
548
+ - Use admin@example.com / Admin123!
549
+ - Should authenticate successfully
550
+
551
+ 2. **Test Product Sync**:
552
+ - Click "Sync Products"
553
+ - Should sync from Stripe
554
+
555
+ 3. **Test Authorization**:
556
+ - Try with non-admin user
557
+ - Should show "Admin Access Required"
558
+
559
+ ## Next Step
560
+
561
+ When ALL todos are ✅ complete and admin panel works:
562
+
563
+ ```javascript
564
+ mcp__product-manager__get_agent_instructions({
565
+ category: "spaps-integration",
566
+ project: "spaps-demo",
567
+ step: 10
568
+ })
569
+ ```
570
+
571
+ ## Summary
572
+
573
+ Admin operations provide:
574
+ - **Full system control** - Manage users and products
575
+ - **Statistics dashboard** - Monitor system health
576
+ - **Stripe synchronization** - Keep products in sync
577
+ - **Audit logging** - Track all admin actions
578
+
579
+ Remember: Always authenticate as admin first!