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,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!
|