claudmax 2.0.0 → 2.0.1

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 (124) hide show
  1. package/claudmax-1.0.16.tgz +0 -0
  2. package/{packages/cli/index.js → index.js} +2 -0
  3. package/package.json +27 -55
  4. package/.claude/settings.local.json +0 -7
  5. package/.env.example +0 -24
  6. package/.github/workflows/publish.yml +0 -31
  7. package/README.md +0 -178
  8. package/claudmax-mcp-1.0.2.tgz +0 -0
  9. package/help +0 -0
  10. package/help-wal +0 -0
  11. package/next-env.d.ts +0 -6
  12. package/next.config.mjs +0 -43
  13. package/packages/cli/claudmax-1.0.16.tgz +0 -0
  14. package/packages/cli/package.json +0 -33
  15. package/packages/mcp/claudmax-mcp-1.0.0.tgz +0 -0
  16. package/packages/mcp/claudmax-mcp-1.0.1.tgz +0 -0
  17. package/packages/mcp/claudmax-mcp-1.0.2.tgz +0 -0
  18. package/packages/mcp/claudmax-mcp-1.0.3.tgz +0 -0
  19. package/packages/mcp/index.js +0 -129
  20. package/packages/mcp/package-lock.json +0 -1146
  21. package/packages/mcp/package.json +0 -32
  22. package/postcss.config.mjs +0 -6
  23. package/prisma/schema.prisma +0 -130
  24. package/prisma/seed.ts +0 -27
  25. package/public/favicon.svg +0 -10
  26. package/public/robots.txt +0 -10
  27. package/run_build.sh +0 -4
  28. package/scripts/migrate-plans.js +0 -98
  29. package/scripts/seed-blog.ts +0 -1014
  30. package/src/app/admin/dashboard/AdminDashboardClient.tsx +0 -1546
  31. package/src/app/admin/dashboard/page.tsx +0 -13
  32. package/src/app/admin/page.tsx +0 -132
  33. package/src/app/api/admin/auth/me/route.ts +0 -34
  34. package/src/app/api/admin/health/route.ts +0 -110
  35. package/src/app/api/admin/keys/[id]/route.ts +0 -116
  36. package/src/app/api/admin/keys/route.ts +0 -192
  37. package/src/app/api/admin/keys-list/route.ts +0 -81
  38. package/src/app/api/admin/login/route.ts +0 -72
  39. package/src/app/api/admin/logout/route.ts +0 -8
  40. package/src/app/api/admin/migrate/route.ts +0 -133
  41. package/src/app/api/admin/plans/[id]/route.ts +0 -65
  42. package/src/app/api/admin/plans/route.ts +0 -66
  43. package/src/app/api/admin/posts/[id]/route.ts +0 -81
  44. package/src/app/api/admin/posts/route.ts +0 -83
  45. package/src/app/api/admin/seed/route.ts +0 -145
  46. package/src/app/api/admin/settings/route.ts +0 -44
  47. package/src/app/api/admin/stats/route.ts +0 -74
  48. package/src/app/api/admin/users/[id]/route.ts +0 -166
  49. package/src/app/api/admin/users/plans/route.ts +0 -45
  50. package/src/app/api/admin/users/route.ts +0 -202
  51. package/src/app/api/blog/[slug]/route.ts +0 -22
  52. package/src/app/api/blog/route.ts +0 -40
  53. package/src/app/api/cron/daily-status/route.ts +0 -208
  54. package/src/app/api/support/chat/route.ts +0 -55
  55. package/src/app/api/support/chat/session/route.ts +0 -62
  56. package/src/app/api/support/chat/stream/route.ts +0 -44
  57. package/src/app/api/support/email/route.ts +0 -63
  58. package/src/app/api/tools/understand_image/route.ts +0 -113
  59. package/src/app/api/tools/upload/route.ts +0 -179
  60. package/src/app/api/tools/web_search/route.ts +0 -99
  61. package/src/app/api/v1/audio/route.ts +0 -67
  62. package/src/app/api/v1/audio/speech/route.ts +0 -73
  63. package/src/app/api/v1/chat/completions/route.ts +0 -3
  64. package/src/app/api/v1/chat/route.ts +0 -1079
  65. package/src/app/api/v1/images/generations/route.ts +0 -93
  66. package/src/app/api/v1/info/route.ts +0 -30
  67. package/src/app/api/v1/key-status/route.ts +0 -109
  68. package/src/app/api/v1/key-status/stream/route.ts +0 -135
  69. package/src/app/api/v1/messages/count_tokens/route.ts +0 -22
  70. package/src/app/api/v1/messages/route.ts +0 -807
  71. package/src/app/api/v1/models/route.ts +0 -14
  72. package/src/app/api/v1/route.ts +0 -18
  73. package/src/app/blog/BlogClient.tsx +0 -193
  74. package/src/app/blog/[slug]/page.tsx +0 -117
  75. package/src/app/blog/page.tsx +0 -20
  76. package/src/app/check-usage/CheckUsageClient.tsx +0 -186
  77. package/src/app/check-usage/layout.tsx +0 -11
  78. package/src/app/check-usage/page.tsx +0 -15
  79. package/src/app/docs/layout.tsx +0 -16
  80. package/src/app/docs/page.tsx +0 -1055
  81. package/src/app/faq/FAQClient.tsx +0 -227
  82. package/src/app/faq/page.tsx +0 -21
  83. package/src/app/globals.css +0 -75
  84. package/src/app/layout.tsx +0 -80
  85. package/src/app/page.tsx +0 -256
  86. package/src/app/reseller/ResellerClient.tsx +0 -435
  87. package/src/app/reseller/page.tsx +0 -15
  88. package/src/app/setup.ps1/route.ts +0 -79
  89. package/src/app/setup.sh/route.ts +0 -113
  90. package/src/app/sitemap.ts +0 -50
  91. package/src/app/status/StatusClient.tsx +0 -103
  92. package/src/app/status/layout.tsx +0 -11
  93. package/src/app/status/page.tsx +0 -15
  94. package/src/app/support/SupportClient.tsx +0 -411
  95. package/src/app/support/page.tsx +0 -25
  96. package/src/app/v1/chat/completions/route.ts +0 -3
  97. package/src/app/v1/chat/route.ts +0 -4
  98. package/src/app/v1/messages/route.ts +0 -3
  99. package/src/components/Footer.tsx +0 -120
  100. package/src/components/Header.tsx +0 -131
  101. package/src/components/landing/features.tsx +0 -99
  102. package/src/components/ui/badge.tsx +0 -32
  103. package/src/components/ui/button.tsx +0 -46
  104. package/src/components/ui/card.tsx +0 -50
  105. package/src/components/ui/dialog.tsx +0 -97
  106. package/src/components/ui/dropdown-menu.tsx +0 -156
  107. package/src/components/ui/input.tsx +0 -21
  108. package/src/components/ui/label.tsx +0 -15
  109. package/src/components/ui/separator.tsx +0 -22
  110. package/src/components/ui/switch.tsx +0 -27
  111. package/src/components/ui/tabs.tsx +0 -51
  112. package/src/components/ui/toast.tsx +0 -103
  113. package/src/lib/auth.ts +0 -45
  114. package/src/lib/prisma.ts +0 -20
  115. package/src/lib/providers.ts +0 -158
  116. package/src/lib/security.ts +0 -165
  117. package/src/lib/utils.ts +0 -14
  118. package/src/middleware.ts +0 -30
  119. package/tailwind.config.ts +0 -53
  120. package/tsconfig.json +0 -41
  121. package/tsconfig.tsbuildinfo +0 -1
  122. package/vercel.json +0 -8
  123. /package/{packages/cli/bin → bin}/claudmax.js +0 -0
  124. /package/{packages/cli/claudmax-1.0.17.tgz → claudmax-1.0.17.tgz} +0 -0
@@ -1,435 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useCallback } from 'react';
4
- import Header from '@/components/Header';
5
- import Footer from '@/components/Footer';
6
-
7
- interface ApiKey {
8
- id: string;
9
- key: string;
10
- name: string;
11
- prefix: string;
12
- tier: string;
13
- isActive: boolean;
14
- blockedUntil: string | null;
15
- tokenLimitOverride: number | null;
16
- displayMultiplier: number;
17
- windowTokensUsed: number;
18
- windowRequestsUsed: number;
19
- windowStartAt: string | null;
20
- createdAt: string;
21
- lastUsedAt: string | null;
22
- expiresAt: string | null;
23
- planId: string | null;
24
- plan: { id: string; name: string; tier: string; durationDays: number } | null;
25
- }
26
-
27
- interface MeData {
28
- id: string;
29
- username: string;
30
- name: string;
31
- role: string;
32
- keysGenLimit: number;
33
- keysGenToday: number;
34
- keysGenResetAt: string | null;
35
- }
36
-
37
- function fmtTokens(n: number) {
38
- if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
39
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
40
- if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
41
- return String(n);
42
- }
43
-
44
- function StatCard({ label, value, color }: { label: string; value: string | number; color?: string }) {
45
- return (
46
- <div className="bg-white rounded-2xl border border-gray-200 p-5 card-shadow flex flex-col items-center justify-center">
47
- <p className="text-3xl font-black text-[#111827]">{value}</p>
48
- <p className={`text-xs font-bold uppercase tracking-wider mt-1 ${color ?? 'text-[#9CA3AF]'}`}>{label}</p>
49
- </div>
50
- );
51
- }
52
-
53
- function ExtendDaysModal({ key_, onConfirm, onCancel }: { key_: ApiKey; onConfirm: (days: number) => void; onCancel: () => void }) {
54
- const durations = [
55
- { label: '+7 Days', value: 7 },
56
- { label: '+15 Days', value: 15 },
57
- { label: '+30 Days', value: 30 },
58
- { label: '+90 Days', value: 90 },
59
- { label: '−7 Days', value: -7 },
60
- { label: '−15 Days', value: -15 },
61
- ];
62
- return (
63
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
64
- <div className="bg-white rounded-2xl border border-gray-200 w-full max-w-xs p-6 card-shadow">
65
- <h3 className="text-lg font-bold text-[#111827] mb-1">Extend / Reduce Plan</h3>
66
- <p className="text-[#9CA3AF] text-xs font-mono mb-4">For key: {key_.name}</p>
67
- <div className="space-y-2">
68
- {durations.map(d => (
69
- <button
70
- key={d.value}
71
- onClick={() => onConfirm(d.value)}
72
- className={`w-full h-10 rounded-xl border font-semibold text-sm text-left px-3 transition-colors ${d.value < 0 ? 'hover:bg-red-50 hover:border-red-300 hover:text-red-600 border-gray-200 text-[#374151]' : 'hover:bg-purple-50 hover:border-purple-300 hover:text-purple-700 border-gray-200 text-[#374151]'}`}
73
- >
74
- {d.label}
75
- </button>
76
- ))}
77
- </div>
78
- <button onClick={onCancel} className="mt-3 w-full h-10 rounded-xl border border-gray-200 text-[#6B7280] font-semibold text-sm hover:bg-gray-50 transition-colors">Cancel</button>
79
- </div>
80
- </div>
81
- );
82
- }
83
-
84
- function ConfirmModal({ title, message, onConfirm, onCancel, danger }: { title: string; message: string; onConfirm: () => void; onCancel: () => void; danger?: boolean }) {
85
- return (
86
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
87
- <div className="bg-white rounded-2xl border border-gray-200 w-full max-w-sm p-6 card-shadow-md">
88
- <h3 className="text-lg font-bold text-[#111827] mb-2">{title}</h3>
89
- <p className="text-[#6B7280] text-sm mb-6">{message}</p>
90
- <div className="flex gap-3">
91
- <button onClick={onCancel} className="flex-1 h-10 rounded-xl border border-gray-200 text-[#6B7280] font-semibold text-sm hover:bg-gray-50 transition-colors">Cancel</button>
92
- <button onClick={onConfirm} className={`flex-1 h-10 rounded-xl font-semibold text-sm text-white transition-colors ${danger ? 'bg-red-500 hover:bg-red-600' : 'bg-[#5244F3] hover:bg-[#4338CA]'}`}>Confirm</button>
93
- </div>
94
- </div>
95
- </div>
96
- );
97
- }
98
-
99
- export default function ResellerPage() {
100
- const [me, setMe] = useState<MeData | null>(null);
101
- const [keys, setKeys] = useState<ApiKey[]>([]);
102
- const [total, setTotal] = useState(0);
103
- const [search, setSearch] = useState('');
104
- const [tierFilter, setTierFilter] = useState('');
105
- const [statusFilter, setStatusFilter] = useState('');
106
- const [loading, setLoading] = useState(true);
107
- const [copied, setCopied] = useState('');
108
- const [confirm, setConfirm] = useState<{ title: string; message: string; onConfirm: () => void; danger?: boolean } | null>(null);
109
- const [extendKey, setExtendKey] = useState<ApiKey | null>(null);
110
- const [actionLoading, setActionLoading] = useState<string | null>(null);
111
- const [newlyCreated, setNewlyCreated] = useState('');
112
-
113
- const fetchKeys = useCallback(async () => {
114
- setLoading(true);
115
- try {
116
- const params = new URLSearchParams();
117
- if (search) params.set('search', search);
118
- if (tierFilter) params.set('tier', tierFilter);
119
- if (statusFilter) params.set('status', statusFilter);
120
- const r = await fetch(`/api/admin/keys-list?${params}`);
121
- if (r.ok) {
122
- const d = await r.json();
123
- setKeys(d.keys.filter((k: any) => k.resellerId === me?.id));
124
- setTotal(d.total);
125
- }
126
- } finally { setLoading(false); }
127
- }, [search, tierFilter, statusFilter, me]);
128
-
129
- useEffect(() => {
130
- fetch('/api/admin/auth/me').then(r => r.ok ? r.json() : null).then(data => {
131
- if (!data) { window.location.href = '/admin'; return; }
132
- if (data.role !== 'reseller') { window.location.href = '/admin/dashboard'; return; }
133
- setMe(data);
134
- }).catch(() => window.location.href = '/admin');
135
- }, []);
136
-
137
- useEffect(() => { if (me) fetchKeys(); }, [me, fetchKeys]);
138
-
139
- useEffect(() => {
140
- const t = setTimeout(() => fetchKeys(), 300);
141
- return () => clearTimeout(t);
142
- }, [search, tierFilter, statusFilter, fetchKeys]);
143
-
144
- // Auto-refresh
145
- useEffect(() => {
146
- if (!me) return;
147
- const interval = setInterval(() => fetchKeys(), 15000);
148
- return () => clearInterval(interval);
149
- }, [me, fetchKeys]);
150
-
151
- const doAction = async (id: string, action: string) => {
152
- setActionLoading(id + action);
153
- try {
154
- const r = await fetch(`/api/admin/keys/${id}`, {
155
- method: 'PATCH',
156
- headers: { 'Content-Type': 'application/json' },
157
- body: JSON.stringify({ action }),
158
- });
159
- if (r.ok) { await fetchKeys(); }
160
- } finally { setActionLoading(null); }
161
- };
162
-
163
- const handleExtend = async (days: number) => {
164
- if (!extendKey || !me) return;
165
- setExtendKey(null);
166
- setActionLoading(extendKey.id + 'extend');
167
- try {
168
- await fetch(`/api/admin/keys/${extendKey.id}`, {
169
- method: 'PATCH',
170
- headers: { 'Content-Type': 'application/json' },
171
- body: JSON.stringify({ action: 'extendDays', days }),
172
- });
173
- await fetchKeys();
174
- } finally { setActionLoading(null); }
175
- };
176
-
177
- const handleCreateKey = async () => {
178
- if (!me) return;
179
- if (me.keysGenToday >= me.keysGenLimit) {
180
- setConfirm({ title: 'Key Limit Reached', message: `You've reached your daily limit of ${me.keysGenLimit} keys. The limit resets at midnight.`, onConfirm: () => setConfirm(null) });
181
- return;
182
- }
183
- setActionLoading('creating');
184
- try {
185
- const r = await fetch('/api/admin/keys', {
186
- method: 'POST',
187
- headers: { 'Content-Type': 'application/json' },
188
- body: JSON.stringify({ name: 'New API Key' }),
189
- });
190
- if (r.ok) {
191
- const d = await r.json();
192
- setNewlyCreated(d.key);
193
- await fetchKeys();
194
- // Refresh me to update keysGenToday
195
- const meRes = await fetch('/api/admin/auth/me');
196
- if (meRes.ok) setMe(await meRes.json());
197
- }
198
- } finally { setActionLoading(null); }
199
- };
200
-
201
- if (!me) return (
202
- <div className="min-h-screen bg-[#F9FAFB] flex items-center justify-center">
203
- <div className="w-8 h-8 border-2 border-purple-600 border-t-transparent rounded-full animate-spin" />
204
- </div>
205
- );
206
-
207
- const activeKeys = keys.filter(k => k.isActive && !k.blockedUntil).length;
208
- const disabledKeys = keys.filter(k => !k.isActive && !k.blockedUntil).length;
209
- const expiredKeys = keys.filter(k => k.blockedUntil && new Date(k.blockedUntil) < new Date()).length;
210
- const overLimitKeys = keys.filter(k => {
211
- const limit = getKeyLimit(k);
212
- return k.windowTokensUsed >= limit;
213
- }).length;
214
-
215
- const tierPlans: Record<string, { label: string; limit: number }> = {
216
- free: { label: 'Free', limit: 500_000 },
217
- '5x': { label: '5x Max', limit: 5_000_000 },
218
- '20x': { label: '20x Max', limit: 20_000_000 },
219
- unlimited: { label: 'Unlimited', limit: 999_999_999 },
220
- };
221
-
222
- const TIER_LIMIT_MS = 5 * 60 * 60 * 1000; // 5h in ms
223
-
224
- function getKeyLimit(k: ApiKey) {
225
- const PLAN_LIMITS: Record<string, number> = { Free: 500_000, Mini: 2_000_000, Pro: 5_000_000, 'Max 20x': 20_000_000, Unlimited: 999_999_999 };
226
- if (k.plan) return PLAN_LIMITS[k.plan.name] ?? 500_000;
227
- return k.tokenLimitOverride ?? tierPlans[k.tier]?.limit ?? 500_000;
228
- }
229
- function getKeyLabel(k: ApiKey) {
230
- if (k.plan) return k.plan.name;
231
- return tierPlans[k.tier]?.label ?? k.tier;
232
- }
233
- function getExpiresAt(k: ApiKey): string | null {
234
- if (k.expiresAt) return new Date(k.expiresAt).toLocaleDateString();
235
- return null;
236
- }
237
-
238
- return (
239
- <div className="min-h-screen bg-[#F9FAFB] flex flex-col">
240
- <Header />
241
-
242
- <div className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 py-8 w-full">
243
-
244
- {/* Header */}
245
- <div className="flex items-start justify-between mb-6">
246
- <div>
247
- <h1 className="text-3xl font-black text-[#111827]">My API Keys</h1>
248
- <p className="text-[#9CA3AF] text-sm mt-1">Manage API keys for your applications or clients.</p>
249
- </div>
250
- <div className="flex items-center gap-2">
251
- <span className="text-xs font-bold px-3 py-1.5 rounded-full bg-purple-50 text-purple-700 border border-purple-200">
252
- {me.keysGenToday}/{me.keysGenLimit} keys today
253
- </span>
254
- <button onClick={fetchKeys} className="h-9 px-4 rounded-xl border border-gray-200 text-[#6B7280] font-semibold text-sm hover:bg-gray-50 transition-colors flex items-center gap-1.5">
255
- <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M1 4v6h6M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10M23 14l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>
256
- Refresh
257
- </button>
258
- <button
259
- onClick={handleCreateKey}
260
- disabled={actionLoading === 'creating'}
261
- className="h-9 px-4 bg-[#5244F3] hover:bg-[#4338CA] disabled:bg-gray-300 text-white rounded-xl text-sm font-semibold transition-colors shadow-sm flex items-center gap-1.5"
262
- >
263
- <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M12 5v14M5 12h14"/></svg>
264
- Create Key
265
- </button>
266
- </div>
267
- </div>
268
-
269
- {/* Stat cards */}
270
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
271
- <StatCard label="TOTAL" value={total} />
272
- <StatCard label="ACTIVE" value={activeKeys} color="text-emerald-600" />
273
- <StatCard label="DISABLED" value={disabledKeys} />
274
- <StatCard label="OVER LIMIT" value={overLimitKeys} color={overLimitKeys > 0 ? 'text-red-500' : 'text-[#9CA3AF]'} />
275
- </div>
276
-
277
- {/* Filters */}
278
- <div className="flex items-center gap-2 mb-4">
279
- <div className="relative flex-1 max-w-xs">
280
- <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#D1D5DB]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
281
- <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search by prefix or name..." className="h-9 pl-9 pr-4 bg-white border border-gray-200 rounded-xl text-sm text-[#111827] placeholder:text-[#D1D5DB] focus:outline-none focus:border-purple-400 w-full transition-all" />
282
- </div>
283
- <select value={tierFilter} onChange={e => setTierFilter(e.target.value)} className="h-9 px-3 bg-white border border-gray-200 rounded-xl text-sm text-[#6B7280] focus:outline-none transition-all">
284
- <option value="">All Plans</option>
285
- <option value="free">Free</option>
286
- <option value="5x">5x Max</option>
287
- <option value="20x">20x Max</option>
288
- <option value="unlimited">Unlimited</option>
289
- </select>
290
- <select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="h-9 px-3 bg-white border border-gray-200 rounded-xl text-sm text-[#6B7280] focus:outline-none transition-all">
291
- <option value="">All Status</option>
292
- <option value="active">Active</option>
293
- <option value="inactive">Disabled</option>
294
- </select>
295
- </div>
296
-
297
- {/* Table */}
298
- <div className="bg-white rounded-2xl border border-gray-200 overflow-hidden card-shadow">
299
- <div className="overflow-x-auto">
300
- <table className="w-full text-sm">
301
- <thead>
302
- <tr className="border-b border-gray-100 bg-gray-50">
303
- {['Key', 'Plan', 'Status', 'Usage', 'Actions'].map(h => (
304
- <th key={h} className="text-left text-[#9CA3AF] font-bold px-4 py-3 text-xs uppercase tracking-wider whitespace-nowrap">{h}</th>
305
- ))}
306
- </tr>
307
- </thead>
308
- <tbody>
309
- {loading ? (
310
- Array.from({ length: 3 }).map((_, i) => (
311
- <tr key={i} className="border-b border-gray-100 last:border-0">
312
- {Array.from({ length: 5 }).map((_, j) => (<td key={j} className="px-4 py-4"><div className="h-4 bg-gray-100 rounded animate-pulse w-32" /></td>))}
313
- </tr>
314
- ))
315
- ) : keys.length === 0 ? (
316
- <tr><td colSpan={6} className="px-4 py-16 text-center text-[#D1D5DB] text-sm">No API keys found.</td></tr>
317
- ) : (
318
- keys.map(k => {
319
- const limit = getKeyLimit(k);
320
- const label = getKeyLabel(k);
321
- const used = k.windowTokensUsed ?? 0;
322
- const pct = Math.min(100, (used / limit) * 100);
323
- const isOver = used >= limit;
324
- const isActive = k.isActive && (!k.blockedUntil || new Date(k.blockedUntil) < new Date());
325
- const expires = getExpiresAt(k);
326
-
327
- return (
328
- <tr key={k.id} className="border-b border-gray-100 last:border-0 hover:bg-gray-50/50 transition-colors">
329
- <td className="px-4 py-4">
330
- <p className="text-purple-600 font-bold text-sm">{k.name}</p>
331
- <div className="flex items-center gap-2 mt-0.5">
332
- <code className="text-[#9CA3AF] text-xs font-mono">{k.prefix}••••</code>
333
- <button onClick={() => { navigator.clipboard.writeText(k.key); setCopied(k.id); setTimeout(() => setCopied(''), 2000); }} className="text-[#9CA3AF] hover:text-[#6B7280] transition-colors">
334
- {copied === k.id ? (
335
- <svg className="w-3.5 h-3.5 text-emerald-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20 6L9 17l-5-5"/></svg>
336
- ) : (
337
- <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
338
- )}
339
- </button>
340
- </div>
341
- {expires && <p className="text-[#9CA3AF] text-xs mt-0.5">Expires: {expires}</p>}
342
- </td>
343
- <td className="px-4 py-4">
344
- <span className="text-xs font-bold px-2.5 py-1 rounded-full bg-gray-100 text-gray-600 border border-gray-200">{label}</span>
345
- </td>
346
- <td className="px-4 py-4">
347
- <div className="flex items-center gap-2">
348
- <span className={`text-xs font-bold px-2.5 py-1 rounded-full flex items-center gap-1 ${isActive ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' : 'bg-red-50 text-red-500 border border-red-100'}`}>
349
- <span className={`w-1.5 h-1.5 rounded-full ${isActive ? 'bg-emerald-500' : 'bg-red-500'}`} />
350
- {k.blockedUntil ? 'Blocked' : isActive ? 'Active' : 'Disabled'}
351
- </span>
352
- </div>
353
- {expires && (
354
- <p className="text-[#9CA3AF] text-xs mt-1 flex items-center gap-1">
355
- <svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
356
- {expires}
357
- </p>
358
- )}
359
- </td>
360
- <td className="px-4 py-4 min-w-48">
361
- <p className={`text-sm font-mono font-semibold ${isOver ? 'text-red-500' : 'text-[#374151]'}`}>
362
- {fmtTokens(used)} / {fmtTokens(limit)}
363
- </p>
364
- <div className="h-1.5 bg-gray-100 rounded-full mt-1.5 overflow-hidden">
365
- <div className={`h-full rounded-full transition-all ${isOver ? 'bg-red-500' : pct >= 80 ? 'bg-amber-500' : 'bg-purple-500'}`} style={{ width: `${pct}%` }} />
366
- </div>
367
- </td>
368
- <td className="px-4 py-4">
369
- <div className="flex items-center gap-1">
370
- <button
371
- onClick={() => setExtendKey(k)}
372
- className="h-8 px-3 rounded-lg border border-gray-200 text-[#6B7280] text-xs font-semibold hover:bg-purple-50 hover:border-purple-300 hover:text-purple-700 transition-colors"
373
- >
374
- Extend Days
375
- </button>
376
- {isActive ? (
377
- <button
378
- onClick={() => setConfirm({ title: 'Block Key', message: `Block "${k.name}"? It will stop accepting requests.`, onConfirm: () => { setConfirm(null); doAction(k.id, 'block'); }, danger: true })}
379
- disabled={actionLoading === k.id + 'block'}
380
- className="w-8 h-8 rounded-lg border border-gray-200 flex items-center justify-center text-[#9CA3AF] hover:text-red-500 hover:border-red-200 transition-colors"
381
- title="Block"
382
- >
383
- <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M4.93 4.93l14.14 14.14"/></svg>
384
- </button>
385
- ) : (
386
- <button
387
- onClick={() => doAction(k.id, 'unblock')}
388
- disabled={actionLoading === k.id + 'unblock'}
389
- className="w-8 h-8 rounded-lg border border-gray-200 flex items-center justify-center text-[#9CA3AF] hover:text-emerald-500 hover:border-emerald-200 transition-colors"
390
- title="Unblock"
391
- >
392
- <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M9 12l2 2 4-4"/></svg>
393
- </button>
394
- )}
395
- </div>
396
- </td>
397
- </tr>
398
- );
399
- })
400
- )}
401
- </tbody>
402
- </table>
403
- </div>
404
- </div>
405
- </div>
406
-
407
- <Footer />
408
-
409
- {/* Modals */}
410
- {confirm && (
411
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
412
- <div className="bg-white rounded-2xl border border-gray-200 w-full max-w-sm p-6 card-shadow-md">
413
- <h3 className="text-lg font-bold text-[#111827] mb-2">{confirm.title}</h3>
414
- <p className="text-[#6B7280] text-sm mb-6">{confirm.message}</p>
415
- <button onClick={confirm.onConfirm} className="w-full h-10 rounded-xl bg-[#5244F3] hover:bg-[#4338CA] text-white font-semibold text-sm transition-colors">OK</button>
416
- </div>
417
- </div>
418
- )}
419
- {extendKey && <ExtendDaysModal key_={extendKey} onConfirm={handleExtend} onCancel={() => setExtendKey(null)} />}
420
-
421
- {/* Newly created toast */}
422
- {newlyCreated && (
423
- <div className="fixed bottom-6 right-6 z-50 bg-white rounded-2xl border border-amber-200 shadow-xl p-4 max-w-sm w-full">
424
- <p className="text-amber-600 text-xs font-bold uppercase tracking-wider mb-1">Save your API key</p>
425
- <p className="text-[#374151] text-sm font-medium mb-2">Copy it now — you won't see it again.</p>
426
- <code className="text-purple-600 font-mono text-xs break-all block mb-2">{newlyCreated}</code>
427
- <div className="flex gap-2">
428
- <button onClick={() => { navigator.clipboard.writeText(newlyCreated); setNewlyCreated(''); }} className="flex-1 h-8 rounded-lg bg-purple-100 hover:bg-purple-200 text-purple-700 text-xs font-semibold transition-colors">Copy & Close</button>
429
- <button onClick={() => setNewlyCreated('')} className="flex-1 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 text-[#6B7280] text-xs font-semibold transition-colors">Close</button>
430
- </div>
431
- </div>
432
- )}
433
- </div>
434
- );
435
- }
@@ -1,15 +0,0 @@
1
- import type { Metadata } from 'next';
2
- import ResellerClient from './ResellerClient';
3
-
4
- export const metadata: Metadata = {
5
- title: 'My API Keys | ClaudMax Reseller Portal',
6
- description: 'Manage your ClaudMax API keys as a reseller. Create, extend, block, and monitor usage for all your client keys from one dashboard.',
7
- keywords: 'ClaudMax reseller, API key management, reseller portal, API key dashboard, manage API keys',
8
- openGraph: { title: 'My API Keys | ClaudMax', description: 'Reseller portal for managing ClaudMax API keys.', type: 'website', url: 'https://claudmax.pro/reseller', siteName: 'ClaudMax' },
9
- twitter: { card: 'summary_large_image', title: 'My API Keys | ClaudMax' },
10
- robots: { index: false, follow: false },
11
- };
12
-
13
- export default function ResellerPage() {
14
- return <ResellerClient />;
15
- }
@@ -1,79 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
-
3
- const SCRIPT = `# ClaudMax CLI Installer for Windows (PowerShell)
4
- # Usage: irm https://claudmax.pro/setup.ps1 | iex
5
-
6
- \$ErrorActionPreference = "Stop"
7
-
8
- \$GREEN = "\x1b[32m"
9
- \$RED = "\x1b[31m"
10
- \$YELLOW = "\x1b[33m"
11
- \$CYAN = "\x1b[36m"
12
- \$ORANGE = "\x1b[33m"
13
- \$BOLD = "\x1b[1m"
14
- \$DIM = "\x1b[2m"
15
- \$RESET = "\x1b[0m"
16
-
17
- \$V = "\$GREEN\$V\$RESET"
18
- \$X = "\$RED\$X\$RESET"
19
- \$W = "\$YELLOW\$W\$RESET"
20
- \$A = "\$CYAN>\$RESET"
21
-
22
- function Write-Banner {
23
- Write-Host ""
24
- Write-Host "\$ORANGE\`n╔══════════════════════════════════════════════════════════╗\$RESET"
25
- Write-Host "\$ORANGE║\$RESET" -NoNewline; Write-Host " \$BOLD ClaudMax Setup Wizard\$RESET" -NoNewline; Write-Host "\$ORANGE║\$RESET"
26
- Write-Host "\$ORANGE╚══════════════════════════════════════════════════════════╝\$RESET"
27
- Write-Host ""
28
- }
29
-
30
- function Test-Command(\$cmd) {
31
- try { Get-Command \$cmd -ErrorAction Stop | Out-Null; return \$true } catch { return \$false }
32
- }
33
-
34
- Write-Banner
35
-
36
- Write-Host "\$BOLD Checking dependencies... \$RESET"
37
-
38
- if (Test-Command node) {
39
- \$v = (node --version)
40
- Write-Host " \$V Node.js \$DIM\$v\$RESET"
41
- } else {
42
- Write-Host " \$X Node.js \$DIMnot found\$RESET"
43
- Write-Host "\$A Installing Node.js via winget..."
44
- winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements 2>\$null
45
- }
46
-
47
- if (Test-Command git) {
48
- \$v = (git --version)
49
- Write-Host " \$V Git \$DIM\$v\$RESET"
50
- } else {
51
- Write-Host " \$X Git \$DIMnot found\$RESET"
52
- Write-Host "\$A Installing Git via winget..."
53
- winget install Git.Git --accept-package-agreements --accept-source-agreements 2>\$null
54
- }
55
-
56
- Write-Host ""
57
-
58
- # Refresh PATH so node/git are available
59
- \$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
60
-
61
- Write-Host "\$A Running ClaudMax CLI installer..."
62
- Write-Host ""
63
- Write-Host "\$CYANGet Your API Key: https://www.claudmax.pro/support\$RESET"
64
- Write-Host ""
65
-
66
- # Run the full interactive installer via npx
67
- npx claudmax
68
- `;
69
-
70
- export async function GET() {
71
- return new NextResponse(SCRIPT, {
72
- status: 200,
73
- headers: {
74
- 'Content-Type': 'text/plain; charset=utf-8',
75
- 'Cache-Control': 'no-cache',
76
- 'Content-Disposition': 'attachment; filename="claudmax-setup.ps1"',
77
- },
78
- });
79
- }
@@ -1,113 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
-
3
- const SCRIPT = `#!/bin/bash
4
- # ClaudMax CLI Installer for macOS & Linux
5
- # Usage: curl -fsSL https://claudmax.pro/setup.sh | bash
6
-
7
- set -e
8
-
9
- BOLD='\\033[1m'
10
- CYAN='\\033[0;36m'
11
- GREEN='\\033[0;32m'
12
- ORANGE='\\033[0;33m'
13
- RED='\\033[0;31m'
14
- DIM='\\033[2m'
15
- MAGENTA='\\033[0;35m'
16
- RESET='\\033[0m'
17
-
18
- CHECK="\${GREEN}✓\${RESET}"
19
- CROSS="\${RED}✗\${RESET}"
20
- ARROW="\${MAGENTA}▶\${RESET}"
21
-
22
- print_banner() {
23
- echo ""
24
- echo -e "\${CYAN}╔════════════════════════════════════════════════════╗\${RESET}"
25
- echo -e "\${CYAN}║\${RESET} \${BOLD}✦ ClaudMax Setup\${RESET} \${CYAN}║\${RESET}"
26
- echo -e "\${CYAN}╚════════════════════════════════════════════════════╝\${RESET}"
27
- echo ""
28
- }
29
-
30
- check_command() {
31
- if command -v "$1" &> /dev/null; then
32
- local ver=$("$1" --version 2>/dev/null | head -1)
33
- echo -e " \${CHECK} \${GREEN}$1\${RESET} \${DIM}\${ver}\${RESET}"
34
- return 0
35
- else
36
- echo -e " \${CROSS} \${RED}$1\${RESET} \${DIM}not found\${RESET}"
37
- return 1
38
- fi
39
- }
40
-
41
- install_nodejs() {
42
- echo -e "\${ARROW} \${BOLD}Installing Node.js...\${RESET}"
43
- if command -v brew &> /dev/null; then
44
- brew install node
45
- elif command -v apt-get &> /dev/null; then
46
- curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs
47
- elif command -v yum &> /dev/null; then
48
- curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - && sudo yum install -y nodejs
49
- else
50
- echo -e " \${CROSS} No package manager found. Install manually: https://nodejs.org"
51
- exit 1
52
- fi
53
- }
54
-
55
- install_git() {
56
- echo -e "\${ARROW} \${BOLD}Installing Git...\${RESET}"
57
- if command -v brew &> /dev/null; then
58
- brew install git
59
- elif command -v apt-get &> /dev/null; then
60
- sudo apt-get update && sudo apt-get install -y git
61
- elif command -v yum &> /dev/null; then
62
- sudo yum install -y git
63
- fi
64
- }
65
-
66
- install_claude() {
67
- echo -e "\${ARROW} \${BOLD}Installing Claude Code CLI...\${RESET}"
68
- npm install -g @anthropic-ai/claude-code 2>/dev/null || echo -e " \${DIM}Could not auto-install — run: npm i -g @anthropic-ai/claude-code\${RESET}"
69
- }
70
-
71
- print_banner
72
-
73
- echo -e "\${BOLD}Checking dependencies...\${RESET}"
74
- MISSING=""
75
-
76
- if ! check_command node; then MISSING="\${MISSING} node"; fi
77
- if ! check_command git; then MISSING="\${MISSING} git"; fi
78
- if ! check_command claude; then MISSING="\${MISSING} claude"; fi
79
-
80
- echo ""
81
-
82
- if [[ "$MISSING" == *"node"* ]]; then
83
- install_nodejs
84
- fi
85
-
86
- if [[ "$MISSING" == *"git"* ]]; then
87
- install_git
88
- fi
89
-
90
- if [[ "$MISSING" == *"claude"* ]]; then
91
- if command -v npm &> /dev/null; then
92
- install_claude
93
- fi
94
- fi
95
-
96
- echo ""
97
- echo -e "\${ARROW} \${BOLD}Running ClaudMax CLI installer...\${RESET}"
98
- echo ""
99
- echo -e "\${CYAN}Get Your API Key: https://www.claudmax.pro/support\${RESET}"
100
- echo ""
101
- npx claudmax
102
- `;
103
-
104
- export async function GET() {
105
- return new NextResponse(SCRIPT, {
106
- status: 200,
107
- headers: {
108
- 'Content-Type': 'text/plain; charset=utf-8',
109
- 'Cache-Control': 'no-cache',
110
- 'Content-Disposition': 'attachment; filename="claudmax-setup.sh"',
111
- },
112
- });
113
- }