costhawk 1.0.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.
Files changed (171) hide show
  1. package/.claude/settings.local.json +32 -0
  2. package/STRATEGIC_PLAN_2025-12-01.md +934 -0
  3. package/costcanary/.claude/settings.local.json +9 -0
  4. package/costcanary/.env.production.template +38 -0
  5. package/costcanary/.eslintrc.json +22 -0
  6. package/costcanary/.nvmrc +1 -0
  7. package/costcanary/.prettierignore +11 -0
  8. package/costcanary/.prettierrc.json +12 -0
  9. package/costcanary/ADMIN_SETUP.md +68 -0
  10. package/costcanary/CLAUDE.md +228 -0
  11. package/costcanary/CLERK_SETUP.md +69 -0
  12. package/costcanary/DATABASE_SETUP.md +136 -0
  13. package/costcanary/DEMO_CHECKLIST.md +62 -0
  14. package/costcanary/DEPLOYMENT.md +31 -0
  15. package/costcanary/PRODUCTION_RECOVERY.md +109 -0
  16. package/costcanary/README.md +247 -0
  17. package/costcanary/STRIPE_SECURITY_AUDIT.md +123 -0
  18. package/costcanary/TESTING_ADMIN.md +92 -0
  19. package/costcanary/app/(auth)/sign-in/[[...sign-in]]/page.tsx +25 -0
  20. package/costcanary/app/(auth)/sign-up/[[...sign-up]]/page.tsx +25 -0
  21. package/costcanary/app/(dashboard)/dashboard/admin/page.tsx +260 -0
  22. package/costcanary/app/(dashboard)/dashboard/alerts/page.tsx +64 -0
  23. package/costcanary/app/(dashboard)/dashboard/api-keys/page.tsx +231 -0
  24. package/costcanary/app/(dashboard)/dashboard/billing/page.tsx +349 -0
  25. package/costcanary/app/(dashboard)/dashboard/layout.tsx +188 -0
  26. package/costcanary/app/(dashboard)/dashboard/page.tsx +13 -0
  27. package/costcanary/app/(dashboard)/dashboard/playground/page.tsx +605 -0
  28. package/costcanary/app/(dashboard)/dashboard/settings/page.tsx +86 -0
  29. package/costcanary/app/(dashboard)/dashboard/usage/page.tsx +354 -0
  30. package/costcanary/app/(dashboard)/dashboard/wrapped-keys/page.tsx +677 -0
  31. package/costcanary/app/(marketing)/page.tsx +90 -0
  32. package/costcanary/app/(marketing)/pricing/page.tsx +272 -0
  33. package/costcanary/app/admin/pricing-status/page.tsx +338 -0
  34. package/costcanary/app/api/admin/check-pricing/route.ts +127 -0
  35. package/costcanary/app/api/admin/debug/route.ts +44 -0
  36. package/costcanary/app/api/admin/fix-pricing/route.ts +216 -0
  37. package/costcanary/app/api/admin/pricing-jobs/[jobId]/route.ts +48 -0
  38. package/costcanary/app/api/admin/pricing-jobs/route.ts +45 -0
  39. package/costcanary/app/api/admin/trigger-pricing/route.ts +209 -0
  40. package/costcanary/app/api/admin/whoami/route.ts +44 -0
  41. package/costcanary/app/api/auth/clerk/[...nextjs]/route.ts +93 -0
  42. package/costcanary/app/api/debug/wrapped-key/route.ts +51 -0
  43. package/costcanary/app/api/debug-status/route.ts +9 -0
  44. package/costcanary/app/api/debug-version/route.ts +12 -0
  45. package/costcanary/app/api/health/route.ts +14 -0
  46. package/costcanary/app/api/health-simple/route.ts +18 -0
  47. package/costcanary/app/api/keys/route.ts +162 -0
  48. package/costcanary/app/api/keys/wrapped/[id]/revoke/route.ts +86 -0
  49. package/costcanary/app/api/keys/wrapped/[id]/rotate/route.ts +81 -0
  50. package/costcanary/app/api/keys/wrapped/route.ts +241 -0
  51. package/costcanary/app/api/optimizer/preview/route.ts +147 -0
  52. package/costcanary/app/api/optimizer/route.ts +118 -0
  53. package/costcanary/app/api/pricing/models/route.ts +102 -0
  54. package/costcanary/app/api/proxy/[...path]/route.ts +391 -0
  55. package/costcanary/app/api/proxy/anthropic/route.ts +539 -0
  56. package/costcanary/app/api/proxy/google/route.ts +395 -0
  57. package/costcanary/app/api/proxy/openai/route.ts +529 -0
  58. package/costcanary/app/api/simple-test/route.ts +7 -0
  59. package/costcanary/app/api/stripe/checkout/route.ts +201 -0
  60. package/costcanary/app/api/stripe/webhook/route.ts +392 -0
  61. package/costcanary/app/api/test-connection/route.ts +209 -0
  62. package/costcanary/app/api/test-proxy/route.ts +7 -0
  63. package/costcanary/app/api/test-simple/route.ts +20 -0
  64. package/costcanary/app/api/usage/current/route.ts +112 -0
  65. package/costcanary/app/api/usage/stats/route.ts +129 -0
  66. package/costcanary/app/api/usage/stream/route.ts +113 -0
  67. package/costcanary/app/api/usage/summary/route.ts +67 -0
  68. package/costcanary/app/api/usage/trend/route.ts +119 -0
  69. package/costcanary/app/api/ws/route.ts +23 -0
  70. package/costcanary/app/globals.css +280 -0
  71. package/costcanary/app/layout.tsx +87 -0
  72. package/costcanary/components/Header.tsx +85 -0
  73. package/costcanary/components/dashboard/AddApiKeyModal.tsx +264 -0
  74. package/costcanary/components/dashboard/dashboard-content.tsx +329 -0
  75. package/costcanary/components/landing/DashboardPreview.tsx +222 -0
  76. package/costcanary/components/landing/Features.tsx +238 -0
  77. package/costcanary/components/landing/Footer.tsx +83 -0
  78. package/costcanary/components/landing/Hero.tsx +193 -0
  79. package/costcanary/components/landing/Pricing.tsx +250 -0
  80. package/costcanary/components/landing/Testimonials.tsx +248 -0
  81. package/costcanary/components/theme-provider.tsx +8 -0
  82. package/costcanary/components/ui/alert.tsx +59 -0
  83. package/costcanary/components/ui/badge.tsx +36 -0
  84. package/costcanary/components/ui/button.tsx +56 -0
  85. package/costcanary/components/ui/card.tsx +79 -0
  86. package/costcanary/components/ui/dialog.tsx +122 -0
  87. package/costcanary/components/ui/input.tsx +22 -0
  88. package/costcanary/components/ui/label.tsx +26 -0
  89. package/costcanary/components/ui/progress.tsx +28 -0
  90. package/costcanary/components/ui/select.tsx +160 -0
  91. package/costcanary/components/ui/separator.tsx +31 -0
  92. package/costcanary/components/ui/switch.tsx +29 -0
  93. package/costcanary/components/ui/tabs.tsx +55 -0
  94. package/costcanary/components/ui/toast.tsx +127 -0
  95. package/costcanary/components/ui/toaster.tsx +35 -0
  96. package/costcanary/components/ui/use-toast.ts +189 -0
  97. package/costcanary/components.json +17 -0
  98. package/costcanary/debug-wrapped-keys.md +117 -0
  99. package/costcanary/fix-console.sh +30 -0
  100. package/costcanary/lib/admin-auth.ts +226 -0
  101. package/costcanary/lib/admin-security.ts +124 -0
  102. package/costcanary/lib/audit-events.ts +62 -0
  103. package/costcanary/lib/audit.ts +158 -0
  104. package/costcanary/lib/chart-colors.ts +152 -0
  105. package/costcanary/lib/cost-calculator.ts +212 -0
  106. package/costcanary/lib/db-utils.ts +325 -0
  107. package/costcanary/lib/db.ts +14 -0
  108. package/costcanary/lib/encryption.ts +120 -0
  109. package/costcanary/lib/kms.ts +358 -0
  110. package/costcanary/lib/model-alias.ts +180 -0
  111. package/costcanary/lib/pricing.ts +292 -0
  112. package/costcanary/lib/prisma.ts +52 -0
  113. package/costcanary/lib/railway-db.ts +157 -0
  114. package/costcanary/lib/sse-parser.ts +283 -0
  115. package/costcanary/lib/stripe-client.ts +81 -0
  116. package/costcanary/lib/stripe-server.ts +52 -0
  117. package/costcanary/lib/tokens.ts +396 -0
  118. package/costcanary/lib/usage-limits.ts +164 -0
  119. package/costcanary/lib/utils.ts +6 -0
  120. package/costcanary/lib/websocket.ts +153 -0
  121. package/costcanary/lib/wrapped-keys.ts +531 -0
  122. package/costcanary/market-research.md +443 -0
  123. package/costcanary/middleware.ts +48 -0
  124. package/costcanary/next.config.js +43 -0
  125. package/costcanary/nia-sources.md +151 -0
  126. package/costcanary/package-lock.json +12162 -0
  127. package/costcanary/package.json +92 -0
  128. package/costcanary/package.json.backup +89 -0
  129. package/costcanary/postcss.config.js +6 -0
  130. package/costcanary/pricing-worker/.env.example +8 -0
  131. package/costcanary/pricing-worker/README.md +81 -0
  132. package/costcanary/pricing-worker/package-lock.json +1109 -0
  133. package/costcanary/pricing-worker/package.json +26 -0
  134. package/costcanary/pricing-worker/railway.json +13 -0
  135. package/costcanary/pricing-worker/schema.prisma +326 -0
  136. package/costcanary/pricing-worker/src/index.ts +115 -0
  137. package/costcanary/pricing-worker/src/services/pricing-updater.ts +79 -0
  138. package/costcanary/pricing-worker/src/services/tavily-client.ts +474 -0
  139. package/costcanary/pricing-worker/test-tavily.ts +47 -0
  140. package/costcanary/pricing-worker/tsconfig.json +24 -0
  141. package/costcanary/prisma/migrations/001_add_stripe_fields.sql +26 -0
  142. package/costcanary/prisma/schema.prisma +326 -0
  143. package/costcanary/prisma/seed-pricing.ts +133 -0
  144. package/costcanary/public/costhawk-logo.png +0 -0
  145. package/costcanary/railway.json +30 -0
  146. package/costcanary/railway.toml +16 -0
  147. package/costcanary/research-nia.md +298 -0
  148. package/costcanary/research.md +411 -0
  149. package/costcanary/scripts/build-production.js +65 -0
  150. package/costcanary/scripts/check-current-pricing.ts +51 -0
  151. package/costcanary/scripts/check-pricing-data.ts +174 -0
  152. package/costcanary/scripts/create-stripe-prices.js +49 -0
  153. package/costcanary/scripts/fix-pricing-data.ts +135 -0
  154. package/costcanary/scripts/fix-pricing-db.ts +148 -0
  155. package/costcanary/scripts/postinstall.js +58 -0
  156. package/costcanary/scripts/railway-deploy.sh +52 -0
  157. package/costcanary/scripts/run-migration.js +61 -0
  158. package/costcanary/scripts/start-production.js +175 -0
  159. package/costcanary/scripts/test-wrapped-key.ts +85 -0
  160. package/costcanary/scripts/validate-deployment.js +176 -0
  161. package/costcanary/scripts/validate-production.js +119 -0
  162. package/costcanary/server.js.backup +38 -0
  163. package/costcanary/tailwind.config.ts +216 -0
  164. package/costcanary/test-pricing-status.sh +27 -0
  165. package/costcanary/tsconfig.json +42 -0
  166. package/docs/sessions/session-2025-12-01.md +570 -0
  167. package/executive-summary.md +302 -0
  168. package/index.js +1 -0
  169. package/nia-sources.md +163 -0
  170. package/package.json +16 -0
  171. package/research.md +750 -0
@@ -0,0 +1,260 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import { PlayCircle, Clock, CheckCircle, XCircle, Loader2 } from 'lucide-react';
7
+
8
+ interface Job {
9
+ id: string;
10
+ status: string;
11
+ duration: string;
12
+ startedAt: string;
13
+ completedAt: string;
14
+ trigger: string;
15
+ successCount: number;
16
+ failureCount: number;
17
+ error?: string;
18
+ }
19
+
20
+ export default function AdminPage() {
21
+ const [isTriggering, setIsTriggering] = useState(false);
22
+ const [jobs, setJobs] = useState<Job[]>([]);
23
+ const [adminStatus, setAdminStatus] = useState<Record<string, unknown> | null>(null);
24
+ const [lastResult, setLastResult] = useState<Record<string, unknown> | null>(null);
25
+
26
+ const checkAdminStatus = async () => {
27
+ try {
28
+ const response = await fetch('/api/admin/trigger-pricing');
29
+ const data = await response.json();
30
+ setAdminStatus(data);
31
+ } catch (error) {
32
+ console.error('Failed to check admin status:', error);
33
+ }
34
+ };
35
+
36
+ const triggerPricingDiscovery = async (testMode: boolean = true, provider?: string) => {
37
+ setIsTriggering(true);
38
+ setLastResult(null);
39
+
40
+ try {
41
+ const response = await fetch('/api/admin/trigger-pricing', {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ },
46
+ body: JSON.stringify({ testMode, provider }),
47
+ });
48
+
49
+ const result = await response.json();
50
+ setLastResult(result);
51
+
52
+ if (result.success) {
53
+ // Refresh jobs list
54
+ await loadJobs();
55
+ }
56
+ } catch (error) {
57
+ console.error('Failed to trigger pricing discovery:', error);
58
+ setLastResult({ error: 'Failed to trigger pricing discovery' });
59
+ } finally {
60
+ setIsTriggering(false);
61
+ }
62
+ };
63
+
64
+ const loadJobs = async () => {
65
+ try {
66
+ const response = await fetch('/api/admin/pricing-jobs');
67
+ const data = await response.json();
68
+ setJobs(data.jobs || []);
69
+ } catch (error) {
70
+ console.error('Failed to load jobs:', error);
71
+ }
72
+ };
73
+
74
+ const getStatusIcon = (status: string) => {
75
+ switch (status) {
76
+ case 'COMPLETED': return <CheckCircle className="w-4 h-4 text-green-500" />;
77
+ case 'FAILED': return <XCircle className="w-4 h-4 text-red-500" />;
78
+ case 'RUNNING': return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
79
+ default: return <Clock className="w-4 h-4 text-yellow-500" />;
80
+ }
81
+ };
82
+
83
+ const getStatusColor = (status: string) => {
84
+ switch (status) {
85
+ case 'COMPLETED': return 'bg-green-100 text-green-800';
86
+ case 'FAILED': return 'bg-red-100 text-red-800';
87
+ case 'RUNNING': return 'bg-blue-100 text-blue-800';
88
+ default: return 'bg-yellow-100 text-yellow-800';
89
+ }
90
+ };
91
+
92
+ return (
93
+ <div className="max-w-6xl mx-auto space-y-8">
94
+ <div>
95
+ <h1 className="text-3xl font-bold text-white mb-2">Admin Panel</h1>
96
+ <p className="text-slate-400">Manage pricing discovery and view admin operations</p>
97
+ </div>
98
+
99
+ {/* Admin Status */}
100
+ <div className="bg-slate-900 border border-slate-700 rounded-lg p-6">
101
+ <div className="mb-4">
102
+ <h2 className="text-xl font-semibold text-white">Admin Status</h2>
103
+ <p className="text-slate-400">Your current admin permissions and access level</p>
104
+ </div>
105
+ <div>
106
+ <div className="flex gap-4 mb-4">
107
+ <Button onClick={checkAdminStatus} variant="outline" size="sm">
108
+ Check Status
109
+ </Button>
110
+ </div>
111
+
112
+ {adminStatus && (
113
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
114
+ <div>
115
+ <div className="text-sm text-slate-400">Admin Access</div>
116
+ <Badge variant={(adminStatus.isAdmin as boolean) ? "default" : "destructive"}>
117
+ {(adminStatus.isAdmin as boolean) ? "✅ Admin" : "❌ Not Admin"}
118
+ </Badge>
119
+ </div>
120
+ <div>
121
+ <div className="text-sm text-slate-400">Role</div>
122
+ <div className="text-white">{(adminStatus.role as string) || 'None'}</div>
123
+ </div>
124
+ <div>
125
+ <div className="text-sm text-slate-400">Can Trigger Pricing</div>
126
+ <Badge variant={(adminStatus.canTriggerPricing as boolean) ? "default" : "destructive"}>
127
+ {(adminStatus.canTriggerPricing as boolean) ? "✅ Yes" : "❌ No"}
128
+ </Badge>
129
+ </div>
130
+ </div>
131
+ )}
132
+ </div>
133
+ </div>
134
+
135
+ {/* Trigger Controls */}
136
+ <div className="bg-slate-900 border border-slate-700 rounded-lg p-6">
137
+ <div className="mb-4">
138
+ <h2 className="text-xl font-semibold text-white flex items-center gap-2">
139
+ <PlayCircle className="w-5 h-5" />
140
+ Pricing Discovery
141
+ </h2>
142
+ <p className="text-slate-400">Manually trigger AI-powered pricing discovery jobs</p>
143
+ </div>
144
+ <div>
145
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
146
+ <div className="space-y-3">
147
+ <h3 className="text-lg font-semibold text-white">Test Mode</h3>
148
+ <p className="text-sm text-slate-400">Uses simulated data for testing</p>
149
+ <div className="space-y-2">
150
+ <Button
151
+ onClick={() => triggerPricingDiscovery(true)}
152
+ disabled={isTriggering}
153
+ className="w-full"
154
+ >
155
+ {isTriggering ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
156
+ Test All Providers
157
+ </Button>
158
+ <Button
159
+ onClick={() => triggerPricingDiscovery(true, 'OpenAI')}
160
+ disabled={isTriggering}
161
+ variant="outline"
162
+ className="w-full"
163
+ >
164
+ Test OpenAI Only
165
+ </Button>
166
+ </div>
167
+ </div>
168
+
169
+ <div className="space-y-3">
170
+ <h3 className="text-lg font-semibold text-white">Live Mode</h3>
171
+ <p className="text-sm text-slate-400">Uses real Tavily API to discover pricing</p>
172
+ <div className="space-y-2">
173
+ <Button
174
+ onClick={() => triggerPricingDiscovery(false)}
175
+ disabled={isTriggering}
176
+ variant="destructive"
177
+ className="w-full"
178
+ >
179
+ {isTriggering ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
180
+ 🔥 Live Discovery (All)
181
+ </Button>
182
+ <Button
183
+ onClick={() => triggerPricingDiscovery(false, 'OpenAI')}
184
+ disabled={isTriggering}
185
+ variant="outline"
186
+ className="w-full"
187
+ >
188
+ Live OpenAI Only
189
+ </Button>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ {/* Last Result */}
195
+ {lastResult && (
196
+ <div className="mt-6 p-4 rounded-lg bg-slate-800 border border-slate-600">
197
+ <h4 className="text-white font-medium mb-2">Last Result</h4>
198
+ <pre className="text-sm text-slate-300 overflow-auto">
199
+ {JSON.stringify(lastResult, null, 2)}
200
+ </pre>
201
+ </div>
202
+ )}
203
+ </div>
204
+ </div>
205
+
206
+ {/* Jobs List */}
207
+ <div className="bg-slate-900 border border-slate-700 rounded-lg p-6">
208
+ <div className="mb-4">
209
+ <h2 className="text-xl font-semibold text-white">Recent Jobs</h2>
210
+ <p className="text-slate-400">History of pricing discovery operations</p>
211
+ </div>
212
+ <div>
213
+ <div className="flex gap-4 mb-4">
214
+ <Button onClick={loadJobs} variant="outline" size="sm">
215
+ Refresh Jobs
216
+ </Button>
217
+ </div>
218
+
219
+ {jobs.length === 0 ? (
220
+ <div className="text-center py-8 text-slate-400">
221
+ No jobs found. Trigger a pricing discovery to see results here.
222
+ </div>
223
+ ) : (
224
+ <div className="space-y-3">
225
+ {jobs.map((job) => (
226
+ <div key={job.id} className="flex items-center justify-between p-4 rounded-lg bg-slate-800 border border-slate-600">
227
+ <div className="flex items-center gap-3">
228
+ {getStatusIcon(job.status)}
229
+ <div>
230
+ <div className="text-white font-medium">
231
+ Job {job.id.slice(-8)}
232
+ </div>
233
+ <div className="text-sm text-slate-400">
234
+ {job.trigger} • {job.duration}
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
+ <div className="flex items-center gap-3">
240
+ <div className="text-right">
241
+ <div className="text-sm text-green-400">
242
+ ✅ {job.successCount} success
243
+ </div>
244
+ <div className="text-sm text-red-400">
245
+ ❌ {job.failureCount} failed
246
+ </div>
247
+ </div>
248
+ <Badge className={getStatusColor(job.status)}>
249
+ {job.status}
250
+ </Badge>
251
+ </div>
252
+ </div>
253
+ ))}
254
+ </div>
255
+ )}
256
+ </div>
257
+ </div>
258
+ </div>
259
+ );
260
+ }
@@ -0,0 +1,64 @@
1
+ "use client"
2
+
3
+ import { motion } from "framer-motion"
4
+ import { Bell, AlertTriangle, TrendingUp } from "lucide-react"
5
+
6
+ export default function AlertsPage() {
7
+ return (
8
+ <div className="space-y-6">
9
+ {/* Header */}
10
+ <div>
11
+ <h1 className="text-3xl font-bold text-white">Budget Alerts</h1>
12
+ <p className="mt-2 text-slate-400">
13
+ Set up alerts to monitor your API spending
14
+ </p>
15
+ </div>
16
+
17
+ {/* Alert Settings */}
18
+ <motion.div
19
+ initial={{ opacity: 0, y: 20 }}
20
+ animate={{ opacity: 1, y: 0 }}
21
+ className="rounded-lg border border-slate-800 bg-slate-900/50 p-6 backdrop-blur-sm"
22
+ >
23
+ <h2 className="mb-4 text-lg font-medium text-white">Alert Preferences</h2>
24
+ <div className="space-y-4">
25
+ <label className="flex items-center justify-between">
26
+ <div className="flex items-center space-x-3">
27
+ <AlertTriangle className="h-5 w-5 text-yellow-500" />
28
+ <div>
29
+ <p className="font-medium text-white">Budget Warning</p>
30
+ <p className="text-sm text-slate-400">Alert when usage reaches 80% of budget</p>
31
+ </div>
32
+ </div>
33
+ <input type="checkbox" defaultChecked className="h-5 w-5 rounded" />
34
+ </label>
35
+
36
+ <label className="flex items-center justify-between">
37
+ <div className="flex items-center space-x-3">
38
+ <TrendingUp className="h-5 w-5 text-orange-500" />
39
+ <div>
40
+ <p className="font-medium text-white">Unusual Activity</p>
41
+ <p className="text-sm text-slate-400">Alert on sudden usage spikes</p>
42
+ </div>
43
+ </div>
44
+ <input type="checkbox" defaultChecked className="h-5 w-5 rounded" />
45
+ </label>
46
+ </div>
47
+ </motion.div>
48
+
49
+ {/* Coming Soon */}
50
+ <motion.div
51
+ initial={{ opacity: 0, y: 20 }}
52
+ animate={{ opacity: 1, y: 0 }}
53
+ transition={{ delay: 0.1 }}
54
+ className="rounded-lg border border-dashed border-slate-700 bg-slate-900/30 p-12 text-center"
55
+ >
56
+ <Bell className="mx-auto h-12 w-12 text-slate-600" />
57
+ <h3 className="mt-4 text-lg font-medium text-white">Advanced Alerts Coming Soon</h3>
58
+ <p className="mt-2 text-slate-400">
59
+ Custom thresholds, email notifications, Slack integration, and more
60
+ </p>
61
+ </motion.div>
62
+ </div>
63
+ )
64
+ }
@@ -0,0 +1,231 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { motion } from "framer-motion"
5
+ import { Plus, Shield, Trash2, Copy, CheckCircle, AlertCircle, Clock, Key } from "lucide-react"
6
+ import { AddApiKeyModal } from "@/components/dashboard/AddApiKeyModal"
7
+
8
+ interface ApiKey {
9
+ id: string
10
+ provider: string
11
+ alias: string | null
12
+ masked: string
13
+ isActive: boolean
14
+ lastUsed: string | null
15
+ createdAt: string
16
+ }
17
+
18
+ export default function ApiKeysPage() {
19
+ const [keys, setKeys] = useState<ApiKey[]>([])
20
+ const [loading, setLoading] = useState(true)
21
+ const [showAddModal, setShowAddModal] = useState(false)
22
+ const [copiedUrl, setCopiedUrl] = useState<string | null>(null)
23
+
24
+ useEffect(() => {
25
+ fetchKeys()
26
+ }, [])
27
+
28
+ const fetchKeys = async () => {
29
+ try {
30
+ const response = await fetch("/api/keys")
31
+ const data = await response.json()
32
+ setKeys(data.keys || [])
33
+ } catch (error) {
34
+ console.error("Failed to fetch API keys:", error)
35
+ } finally {
36
+ setLoading(false)
37
+ }
38
+ }
39
+
40
+ const handleDeleteKey = async (keyId: string) => {
41
+ if (!confirm("Are you sure you want to delete this API key? This action cannot be undone.")) {
42
+ return
43
+ }
44
+
45
+ try {
46
+ const response = await fetch(`/api/keys?id=${keyId}`, {
47
+ method: "DELETE",
48
+ })
49
+
50
+ if (response.ok) {
51
+ setKeys(keys.filter(key => key.id !== keyId))
52
+ }
53
+ } catch (error) {
54
+ console.error("Failed to delete API key:", error)
55
+ }
56
+ }
57
+
58
+ const copyProxyUrl = (provider: string) => {
59
+ const baseUrl = window.location.origin
60
+ const proxyUrl = `${baseUrl}/api/proxy/${provider.toLowerCase()}/`
61
+ navigator.clipboard.writeText(proxyUrl)
62
+ setCopiedUrl(provider)
63
+ setTimeout(() => setCopiedUrl(null), 2000)
64
+ }
65
+
66
+ const getProviderIcon = (provider: string) => {
67
+ const icons: Record<string, string> = {
68
+ OPENAI: "🤖",
69
+ ANTHROPIC: "🧠",
70
+ GOOGLE: "🔍",
71
+ GROK: "🚀",
72
+ }
73
+ return icons[provider] || "🔑"
74
+ }
75
+
76
+ const getStatusIcon = (lastUsed: string | null) => {
77
+ if (!lastUsed) return <Clock className="h-4 w-4 text-slate-500" />
78
+
79
+ const lastUsedDate = new Date(lastUsed)
80
+ const daysSinceUsed = Math.floor((Date.now() - lastUsedDate.getTime()) / (1000 * 60 * 60 * 24))
81
+
82
+ if (daysSinceUsed < 1) {
83
+ return <CheckCircle className="h-4 w-4 text-green-500" />
84
+ } else if (daysSinceUsed < 7) {
85
+ return <Clock className="h-4 w-4 text-yellow-500" />
86
+ } else {
87
+ return <AlertCircle className="h-4 w-4 text-orange-500" />
88
+ }
89
+ }
90
+
91
+ return (
92
+ <>
93
+ <div className="space-y-6">
94
+ {/* Header */}
95
+ <div className="flex items-center justify-between">
96
+ <div>
97
+ <h1 className="text-3xl font-bold text-white">API Keys</h1>
98
+ <p className="mt-2 text-slate-400">
99
+ Manage your API keys securely. All keys are encrypted with AES-256.
100
+ </p>
101
+ </div>
102
+ <motion.button
103
+ whileHover={{ scale: 1.05 }}
104
+ whileTap={{ scale: 0.95 }}
105
+ onClick={() => setShowAddModal(true)}
106
+ className="flex items-center space-x-2 rounded-lg bg-primary-600 px-4 py-2 text-white transition-colors hover:bg-primary-700"
107
+ >
108
+ <Plus className="h-5 w-5" />
109
+ <span>Add API Key</span>
110
+ </motion.button>
111
+ </div>
112
+
113
+ {/* API Keys List */}
114
+ {loading ? (
115
+ <div className="space-y-4">
116
+ {[1, 2, 3].map((i) => (
117
+ <div key={i} className="h-24 animate-pulse rounded-lg bg-slate-900" />
118
+ ))}
119
+ </div>
120
+ ) : keys.length === 0 ? (
121
+ <motion.div
122
+ initial={{ opacity: 0, y: 20 }}
123
+ animate={{ opacity: 1, y: 0 }}
124
+ className="rounded-lg border border-slate-800 bg-slate-900/50 p-12 text-center backdrop-blur-sm"
125
+ >
126
+ <Key className="mx-auto h-12 w-12 text-slate-600" />
127
+ <h3 className="mt-4 text-lg font-medium text-white">No API Keys</h3>
128
+ <p className="mt-2 text-slate-400">
129
+ Add your first API key to start tracking costs
130
+ </p>
131
+ <motion.button
132
+ whileHover={{ scale: 1.05 }}
133
+ whileTap={{ scale: 0.95 }}
134
+ onClick={() => setShowAddModal(true)}
135
+ className="mt-6 rounded-lg bg-primary-600 px-4 py-2 text-white transition-colors hover:bg-primary-700"
136
+ >
137
+ Add Your First Key
138
+ </motion.button>
139
+ </motion.div>
140
+ ) : (
141
+ <div className="space-y-4">
142
+ {keys.map((key, index) => (
143
+ <motion.div
144
+ key={key.id}
145
+ initial={{ opacity: 0, y: 20 }}
146
+ animate={{ opacity: 1, y: 0 }}
147
+ transition={{ delay: index * 0.05 }}
148
+ className="group rounded-lg border border-slate-800 bg-slate-900/50 p-6 backdrop-blur-sm transition-all hover:border-slate-700 hover:bg-slate-900/70"
149
+ >
150
+ <div className="flex items-start justify-between">
151
+ <div className="flex items-start space-x-4">
152
+ <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-slate-800 text-2xl">
153
+ {getProviderIcon(key.provider)}
154
+ </div>
155
+ <div>
156
+ <div className="flex items-center space-x-2">
157
+ <h3 className="text-lg font-medium text-white">
158
+ {key.provider}
159
+ </h3>
160
+ {key.alias && (
161
+ <span className="rounded-full bg-slate-800 px-2 py-1 text-xs text-slate-400">
162
+ {key.alias}
163
+ </span>
164
+ )}
165
+ {getStatusIcon(key.lastUsed)}
166
+ </div>
167
+ <div className="mt-1 flex items-center space-x-2 text-sm text-slate-400">
168
+ <Shield className="h-3 w-3" />
169
+ <span className="font-mono">{key.masked}</span>
170
+ </div>
171
+ <div className="mt-2 text-xs text-slate-500">
172
+ Added {new Date(key.createdAt).toLocaleDateString()}
173
+ {key.lastUsed && (
174
+ <> • Last used {new Date(key.lastUsed).toLocaleDateString()}</>
175
+ )}
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <div className="flex space-x-2 opacity-0 transition-opacity group-hover:opacity-100">
181
+ <motion.button
182
+ whileHover={{ scale: 1.1 }}
183
+ whileTap={{ scale: 0.9 }}
184
+ onClick={() => copyProxyUrl(key.provider)}
185
+ className="rounded-lg bg-slate-800 p-2 text-slate-400 transition-colors hover:bg-slate-700 hover:text-white"
186
+ title="Copy proxy URL"
187
+ >
188
+ {copiedUrl === key.provider ? (
189
+ <CheckCircle className="h-4 w-4 text-green-500" />
190
+ ) : (
191
+ <Copy className="h-4 w-4" />
192
+ )}
193
+ </motion.button>
194
+ <motion.button
195
+ whileHover={{ scale: 1.1 }}
196
+ whileTap={{ scale: 0.9 }}
197
+ onClick={() => handleDeleteKey(key.id)}
198
+ className="rounded-lg bg-slate-800 p-2 text-slate-400 transition-colors hover:bg-red-900/20 hover:text-red-500"
199
+ title="Delete key"
200
+ >
201
+ <Trash2 className="h-4 w-4" />
202
+ </motion.button>
203
+ </div>
204
+ </div>
205
+
206
+ {/* Proxy URL Info */}
207
+ <div className="mt-4 rounded-lg bg-slate-800/50 p-3">
208
+ <p className="text-xs text-slate-500">Proxy URL:</p>
209
+ <p className="mt-1 font-mono text-xs text-slate-400">
210
+ {window.location.origin}/api/proxy/{key.provider.toLowerCase()}/...
211
+ </p>
212
+ </div>
213
+ </motion.div>
214
+ ))}
215
+ </div>
216
+ )}
217
+ </div>
218
+
219
+ {/* Add API Key Modal */}
220
+ {showAddModal && (
221
+ <AddApiKeyModal
222
+ onClose={() => setShowAddModal(false)}
223
+ onSuccess={() => {
224
+ setShowAddModal(false)
225
+ fetchKeys()
226
+ }}
227
+ />
228
+ )}
229
+ </>
230
+ )
231
+ }