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,13 +0,0 @@
1
- import type { Metadata } from 'next';
2
- import AdminDashboardClient from './AdminDashboardClient';
3
-
4
- export const metadata: Metadata = {
5
- title: 'Dashboard | ClaudMax Admin',
6
- description: 'ClaudMax admin dashboard. Manage API keys, monitor usage, view stats, and control reseller access.',
7
- openGraph: { title: 'Dashboard | ClaudMax Admin', type: 'website' },
8
- robots: { index: false, follow: false },
9
- };
10
-
11
- export default function AdminDashboardPage() {
12
- return <AdminDashboardClient />;
13
- }
@@ -1,132 +0,0 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import Link from 'next/link';
5
- import { useRouter } from 'next/navigation';
6
- import Header from '@/components/Header';
7
- import Footer from '@/components/Footer';
8
-
9
- export default function AdminLogin() {
10
- const [username, setUsername] = useState('');
11
- const [password, setPassword] = useState('');
12
- const [error, setError] = useState('');
13
- const [loading, setLoading] = useState(false);
14
- const [showPassword, setShowPassword] = useState(false);
15
- const router = useRouter();
16
-
17
- const login = async (e: React.FormEvent) => {
18
- e.preventDefault();
19
- if (!username.trim() || !password) return;
20
- setLoading(true);
21
- setError('');
22
-
23
- try {
24
- const res = await fetch('/api/admin/login', {
25
- method: 'POST',
26
- headers: { 'Content-Type': 'application/json' },
27
- body: JSON.stringify({ username: username.trim(), password }),
28
- });
29
-
30
- if (res.ok) {
31
- router.push('/admin/dashboard');
32
- } else {
33
- const data = await res.json();
34
- setError(data.error ?? 'Invalid credentials');
35
- }
36
- } catch {
37
- setError('Connection error. Please try again.');
38
- } finally {
39
- setLoading(false);
40
- }
41
- };
42
-
43
- return (
44
- <div className="min-h-screen bg-[#F9FAFB] flex flex-col">
45
- <Header />
46
-
47
- <div className="flex-1 flex items-center justify-center p-4">
48
- <div className="w-full max-w-sm">
49
- {/* Logo */}
50
- <div className="text-center mb-8">
51
- <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-600 to-purple-500 flex items-center justify-center mx-auto mb-4 shadow-md">
52
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" className="text-white">
53
- <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="currentColor"/>
54
- </svg>
55
- </div>
56
- <h1 className="text-2xl font-black text-[#111827]">ClaudMax Admin</h1>
57
- <p className="text-[#9CA3AF] text-sm mt-1">Sign in to manage keys, users, and usage.</p>
58
- </div>
59
-
60
- {/* Login form */}
61
- <div className="bg-white rounded-2xl border border-gray-200 p-6 card-shadow">
62
- <form onSubmit={login} className="space-y-4">
63
- <div>
64
- <label className="text-[#6B7280] text-sm font-medium block mb-1.5">Username</label>
65
- <input
66
- type="text"
67
- value={username}
68
- onChange={(e) => setUsername(e.target.value)}
69
- placeholder="Enter your username"
70
- autoComplete="username"
71
- className="w-full h-11 px-4 bg-[#F9FAFB] border border-gray-200 rounded-xl text-sm text-[#111827] placeholder:text-[#D1D5DB] focus:outline-none focus:border-purple-400 focus:ring-2 focus:ring-purple-100 transition-all"
72
- autoFocus
73
- />
74
- </div>
75
-
76
- <div>
77
- <label className="text-[#6B7280] text-sm font-medium block mb-1.5">Password</label>
78
- <div className="relative">
79
- <input
80
- type={showPassword ? 'text' : 'password'}
81
- value={password}
82
- onChange={(e) => setPassword(e.target.value)}
83
- placeholder="••••••••"
84
- autoComplete="current-password"
85
- className="w-full h-11 px-4 pr-10 bg-[#F9FAFB] border border-gray-200 rounded-xl text-sm text-[#111827] placeholder:text-[#D1D5DB] focus:outline-none focus:border-purple-400 focus:ring-2 focus:ring-purple-100 transition-all"
86
- />
87
- <button
88
- type="button"
89
- onClick={() => setShowPassword(!showPassword)}
90
- className="absolute right-3 top-1/2 -translate-y-1/2 text-[#9CA3AF] hover:text-[#6B7280] transition-colors"
91
- >
92
- {showPassword ? (
93
- <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
94
- ) : (
95
- <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
96
- )}
97
- </button>
98
- </div>
99
- </div>
100
-
101
- {error && (
102
- <div className="p-3 rounded-xl bg-red-50 border border-red-200 text-red-600 text-sm font-medium flex items-center gap-2">
103
- <svg className="w-4 h-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
104
- {error}
105
- </div>
106
- )}
107
-
108
- <button
109
- type="submit"
110
- disabled={loading || !username.trim() || !password}
111
- className="w-full h-11 bg-[#5244F3] hover:bg-[#4338CA] disabled:bg-gray-300 text-white font-semibold rounded-xl text-sm transition-colors shadow-sm flex items-center justify-center gap-2"
112
- >
113
- {loading ? (
114
- <>
115
- <svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
116
- Signing in...
117
- </>
118
- ) : 'Sign In'}
119
- </button>
120
- </form>
121
- </div>
122
-
123
- <p className="text-center text-[#D1D5DB] text-xs mt-6">
124
- <Link href="/" className="hover:text-[#9CA3AF] transition-colors">← Back to ClaudMax</Link>
125
- </p>
126
- </div>
127
- </div>
128
-
129
- <Footer />
130
- </div>
131
- );
132
- }
@@ -1,34 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { getAuthUser } from '@/lib/auth';
3
- import { prisma } from '@/lib/prisma';
4
-
5
- export async function GET() {
6
- const user = await getAuthUser();
7
- if (!user) {
8
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
9
- }
10
- // Fetch fresh from DB to get latest permissions
11
- const admin = await prisma.admin.findUnique({
12
- where: { id: user.id },
13
- select: {
14
- id: true, username: true, name: true, role: true, isActive: true,
15
- canCreateKey: true, canDeleteKey: true, canBlockKey: true,
16
- canManageTokens: true, canCreateReseller: true, canManageResellers: true, canManageUsers: true,
17
- keysGenLimit: true, keysGenToday: true, keysGenResetAt: true,
18
- },
19
- });
20
- if (!admin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
21
-
22
- // Check and reset key-gen window if needed
23
- if (admin.role === 'reseller' && admin.keysGenResetAt) {
24
- if (Date.now() >= admin.keysGenResetAt.getTime()) {
25
- await prisma.admin.update({
26
- where: { id: user.id },
27
- data: { keysGenToday: 0, keysGenResetAt: new Date(Date.now() + 24 * 60 * 60 * 1000) },
28
- });
29
- return NextResponse.json({ ...admin, keysGenToday: 0, keysGenResetAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() });
30
- }
31
- }
32
-
33
- return NextResponse.json(admin);
34
- }
@@ -1,110 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { prisma } from '@/lib/prisma';
3
- import { getAuthUser } from '@/lib/auth';
4
-
5
- export async function GET() {
6
- const me = await getAuthUser();
7
- if (!me) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
8
- if (me.role !== 'super_admin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
9
-
10
- // Check OpenRouter API health
11
- const apiStart = Date.now();
12
- let apiStatus = 'ok';
13
- let apiLatency = 0;
14
- let apiError = '';
15
- try {
16
- const r = await fetch('https://openrouter.ai/api/v1/models', {
17
- headers: { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` },
18
- });
19
- apiLatency = Date.now() - apiStart;
20
- if (!r.ok) {
21
- apiStatus = 'error';
22
- apiError = `HTTP ${r.status}`;
23
- } else {
24
- const data = await r.json();
25
- const models: string[] = Array.isArray(data) ? data.map((m: any) => m.id) : (data.data?.map((m: any) => m.id) ?? []);
26
- apiStatus = 'ok';
27
- }
28
- } catch (err: any) {
29
- apiStatus = 'error';
30
- apiLatency = Date.now() - apiStart;
31
- apiError = err?.message ?? 'Connection failed';
32
- }
33
-
34
- // Stats
35
- const [
36
- totalKeys, activeKeys, totalTokens,
37
- totalUsers, totalAdmins, totalResellers,
38
- ] = await Promise.all([
39
- prisma.apiKey.count(),
40
- prisma.apiKey.count({ where: { isActive: true } }),
41
- prisma.apiKey.aggregate({ _sum: { totalTokensUsed: true } }),
42
- prisma.admin.count(),
43
- prisma.admin.count({ where: { role: { in: ['admin', 'super_admin'] } } }),
44
- prisma.admin.count({ where: { role: 'reseller' } }),
45
- ]);
46
-
47
- // Top keys by usage
48
- const topKeys = await prisma.apiKey.findMany({
49
- orderBy: { totalTokensUsed: 'desc' },
50
- take: 10,
51
- select: {
52
- id: true, name: true, prefix: true, tier: true, isActive: true,
53
- windowTokensUsed: true, windowRequestsUsed: true, totalTokensUsed: true,
54
- windowStartAt: true, lastUsedAt: true,
55
- reseller: { select: { name: true } },
56
- },
57
- });
58
-
59
- // Keys near or over limit (window-based)
60
- const WINDOW_MS = 5 * 60 * 60 * 1000;
61
- const TIER_LIMITS: Record<string, number> = {
62
- free: 500_000,
63
- '5x': 5_000_000,
64
- '20x': 20_000_000,
65
- unlimited: 999_999_999_999,
66
- };
67
-
68
- const overLimitKeys = topKeys.filter(k => {
69
- const limit = TIER_LIMITS[k.tier] ?? 500_000;
70
- return Number(k.windowTokensUsed) >= limit;
71
- });
72
-
73
- // Recent usage (last 5 minutes from now)
74
- const recent = topKeys.filter(k => {
75
- if (!k.windowStartAt) return false;
76
- return Date.now() - k.windowStartAt.getTime() < 5 * 60 * 1000;
77
- });
78
-
79
- return NextResponse.json({
80
- api: {
81
- status: apiStatus,
82
- latency: apiLatency,
83
- error: apiError || undefined,
84
- },
85
- stats: {
86
- totalKeys,
87
- activeKeys,
88
- totalTokens: totalTokens._sum.totalTokensUsed ?? 0,
89
- totalUsers,
90
- totalAdmins,
91
- totalResellers,
92
- },
93
- topKeys: topKeys.map(k => ({
94
- id: k.id,
95
- name: k.name,
96
- prefix: k.prefix,
97
- tier: k.tier,
98
- isActive: k.isActive,
99
- windowTokensUsed: k.windowTokensUsed,
100
- windowRequestsUsed: k.windowRequestsUsed,
101
- totalTokensUsed: k.totalTokensUsed,
102
- windowResetAt: k.windowStartAt ? new Date(k.windowStartAt.getTime() + WINDOW_MS).toISOString() : null,
103
- lastUsedAt: k.lastUsedAt?.toISOString() ?? null,
104
- resellerName: k.reseller?.name ?? null,
105
- isOverLimit: Number(k.windowTokensUsed) >= (TIER_LIMITS[k.tier] ?? 500_000),
106
- })),
107
- overLimitCount: overLimitKeys.length,
108
- activeCount: recent.length,
109
- });
110
- }
@@ -1,116 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { prisma } from '@/lib/prisma';
3
- import { getAuthUser } from '@/lib/auth';
4
-
5
- type Params = { params: Promise<{ id: string }> };
6
-
7
- async function canAccessKey(me: any, keyId: string): Promise<boolean> {
8
- const key = await prisma.apiKey.findUnique({ where: { id: keyId }, select: { resellerId: true } });
9
- if (!key) return false;
10
- if (me.role === 'reseller') return key.resellerId === me.id;
11
- return true;
12
- }
13
-
14
- export async function GET(req: Request, { params }: Params) {
15
- const me = await getAuthUser();
16
- if (!me) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
17
- const { id } = await params;
18
-
19
- if (!await canAccessKey(me, id)) {
20
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
21
- }
22
-
23
- const key = await prisma.apiKey.findUnique({ where: { id } });
24
- if (!key) return NextResponse.json({ error: 'Not found' }, { status: 404 });
25
- return NextResponse.json({
26
- ...key,
27
- windowTokensUsed: key.windowTokensUsed,
28
- totalTokensUsed: key.totalTokensUsed,
29
- });
30
- }
31
-
32
- export async function PATCH(req: Request, { params }: Params) {
33
- const me = await getAuthUser();
34
- if (!me) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
35
- const { id } = await params;
36
- const body = await req.json();
37
- const { action, tier, name, displayMultiplier, tokenLimitOverride, expiresAt, duration, days } = body;
38
-
39
- if (!await canAccessKey(me, id)) {
40
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
41
- }
42
-
43
- if (action === 'block') {
44
- const key = await prisma.apiKey.update({ where: { id }, data: { isActive: false, blockedUntil: null } });
45
- return NextResponse.json({ ...key, windowTokensUsed: Number(key.windowTokensUsed), totalTokensUsed: Number(key.totalTokensUsed) });
46
- }
47
- if (action === 'unblock') {
48
- const key = await prisma.apiKey.update({ where: { id }, data: { isActive: true, blockedUntil: null } });
49
- return NextResponse.json({ ...key, windowTokensUsed: Number(key.windowTokensUsed), totalTokensUsed: Number(key.totalTokensUsed) });
50
- }
51
- if (action === 'tempBlock') {
52
- if (!duration || typeof duration !== 'number' || duration <= 0) {
53
- return NextResponse.json({ error: 'Invalid duration' }, { status: 400 });
54
- }
55
- const blockedUntil = new Date(Date.now() + duration);
56
- const key = await prisma.apiKey.update({ where: { id }, data: { blockedUntil } });
57
- return NextResponse.json({ ...key, windowTokensUsed: Number(key.windowTokensUsed), totalTokensUsed: Number(key.totalTokensUsed), blockedUntil: blockedUntil.toISOString() });
58
- }
59
- if (action === 'extendDays') {
60
- // Resellers can extend/reduce their own key plan by days
61
- if (typeof days !== 'number') {
62
- return NextResponse.json({ error: 'days (number) required' }, { status: 400 });
63
- }
64
- const key = await prisma.apiKey.findUnique({ where: { id } });
65
- if (!key) return NextResponse.json({ error: 'Not found' }, { status: 404 });
66
- if (me.role === 'reseller' && key.resellerId !== me.id) {
67
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
68
- }
69
- const DAY_MS = 24 * 60 * 60 * 1000;
70
- const currentStart = key.windowStartAt ? key.windowStartAt.getTime() : Date.now();
71
- const newStart = currentStart + (days * DAY_MS);
72
- await prisma.apiKey.update({
73
- where: { id },
74
- data: { windowStartAt: new Date(newStart) },
75
- });
76
- return NextResponse.json({ success: true, windowStartAt: new Date(newStart).toISOString() });
77
- }
78
- if (action === 'update') {
79
- // Only super_admin and admin can update token limit, expiresAt, and tier
80
- const isAdmin = me.role === 'super_admin' || me.role === 'admin';
81
- const data: any = {};
82
- if (name !== undefined) data.name = name;
83
- if (displayMultiplier !== undefined) data.displayMultiplier = displayMultiplier;
84
- if (isAdmin) {
85
- if (tier !== undefined) data.tier = tier;
86
- if (tokenLimitOverride !== undefined) {
87
- data.tokenLimitOverride = tokenLimitOverride === null ? null : Number(tokenLimitOverride);
88
- }
89
- if (expiresAt !== undefined) {
90
- data.expiresAt = expiresAt === null ? null : new Date(expiresAt);
91
- }
92
- }
93
- const key = await prisma.apiKey.update({ where: { id }, data });
94
- return NextResponse.json({
95
- ...key,
96
- windowTokensUsed: Number(key.windowTokensUsed),
97
- totalTokensUsed: Number(key.totalTokensUsed),
98
- tokenLimitOverride: key.tokenLimitOverride,
99
- });
100
- }
101
-
102
- return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
103
- }
104
-
105
- export async function DELETE(req: Request, { params }: Params) {
106
- const me = await getAuthUser();
107
- if (!me) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
108
- const { id } = await params;
109
-
110
- if (!await canAccessKey(me, id)) {
111
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
112
- }
113
-
114
- await prisma.apiKey.delete({ where: { id } });
115
- return NextResponse.json({ success: true });
116
- }
@@ -1,192 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { prisma } from '@/lib/prisma';
3
- import { getAuthUser } from '@/lib/auth';
4
- import { generateApiKey } from '@/lib/utils';
5
-
6
- const WINDOW_MS = 5 * 60 * 60 * 1000;
7
-
8
- export async function GET(req: Request) {
9
- const me = await getAuthUser();
10
- if (!me) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
11
-
12
- const { searchParams } = new URL(req.url);
13
- const apiKey = searchParams.get('apiKey');
14
- if (!apiKey) return NextResponse.json({ error: 'API key required' }, { status: 400 });
15
-
16
- const key = await prisma.apiKey.findUnique({ where: { key: apiKey } });
17
- if (!key) return NextResponse.json({ error: 'Invalid API key' }, { status: 401 });
18
-
19
- if (me.role === 'reseller' && key.resellerId !== me.id) {
20
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
21
- }
22
-
23
- // Get plan details
24
- let tokensPerWindow = 500_000;
25
- let requestsPerWindow = 100;
26
- let multiplier = key.displayMultiplier ?? 3.0;
27
-
28
- if (key.planId) {
29
- const plan = await prisma.plan.findUnique({ where: { id: key.planId } });
30
- if (plan) {
31
- tokensPerWindow = Number(plan.tokensPerWindow);
32
- requestsPerWindow = plan.requestsPerWindow;
33
- multiplier = plan.displayMultiplier;
34
- }
35
- }
36
-
37
- const effectiveLimit = Math.floor(tokensPerWindow * multiplier);
38
- const windowStart = key.windowStartAt?.getTime() ?? Date.now();
39
- const windowResetAt = windowStart + WINDOW_MS;
40
-
41
- return NextResponse.json({
42
- key: key.id,
43
- name: key.name,
44
- prefix: key.prefix,
45
- tier: key.tier,
46
- isActive: key.isActive,
47
- displayMultiplier: multiplier,
48
- requestsUsed: key.windowRequestsUsed,
49
- requestsLimit: requestsPerWindow,
50
- tokensUsed: Math.floor(Number(key.windowTokensUsed) * multiplier),
51
- tokensLimit: effectiveLimit,
52
- tokensUsedActual: Number(key.windowTokensUsed),
53
- windowResetAt: new Date(windowResetAt).toISOString(),
54
- expiresAt: key.expiresAt?.toISOString() ?? null,
55
- planId: key.planId,
56
- createdAt: key.createdAt.toISOString(),
57
- lastUsedAt: key.lastUsedAt?.toISOString() ?? null,
58
- });
59
- }
60
-
61
- export async function POST(req: Request) {
62
- const me = await getAuthUser();
63
- if (!me) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
64
-
65
- const { name, planId, resellerId, days, tokenLimitOverride } = await req.json();
66
-
67
- // Fetch the plan
68
- const plan = planId
69
- ? await prisma.plan.findUnique({ where: { id: planId } })
70
- : null;
71
-
72
- // If planId not provided, use default plan for this user
73
- let effectivePlanId = planId;
74
- if (!effectivePlanId) {
75
- if (me.role === 'reseller') {
76
- const admin = await prisma.admin.findUnique({ where: { id: me.id } });
77
- effectivePlanId = admin?.defaultPlanId ?? null;
78
- if (!effectivePlanId) {
79
- // Fall back to first allowed plan
80
- const allowed = admin?.allowedPlanIds ? JSON.parse(admin.allowedPlanIds) : [];
81
- effectivePlanId = allowed[0] ?? null;
82
- }
83
- }
84
- }
85
-
86
- // Permission check: can this user create keys with this plan?
87
- if (me.role === 'reseller' && effectivePlanId) {
88
- const admin = await prisma.admin.findUnique({ where: { id: me.id } });
89
- const allowed: string[] = admin?.allowedPlanIds ? JSON.parse(admin.allowedPlanIds) : [];
90
- if (!allowed.includes(effectivePlanId)) {
91
- return NextResponse.json({ error: 'Plan not allowed for your account' }, { status: 403 });
92
- }
93
- }
94
-
95
- // Reseller assignment
96
- if (resellerId) {
97
- if (me.role === 'reseller') {
98
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
99
- }
100
- if (me.role === 'admin') {
101
- const reseller = await prisma.admin.findUnique({ where: { id: resellerId } });
102
- if (!reseller || reseller.role !== 'reseller') {
103
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
104
- }
105
- }
106
- }
107
-
108
- // Key generation limit check for resellers
109
- if (me.role === 'reseller') {
110
- const admin = await prisma.admin.findUnique({ where: { id: me.id } });
111
- if (admin) {
112
- const now = Date.now();
113
- const resetAt = admin.keysGenResetAt ? admin.keysGenResetAt.getTime() : 0;
114
- if (resetAt === 0 || now >= resetAt) {
115
- await prisma.admin.update({
116
- where: { id: me.id },
117
- data: { keysGenToday: 0, keysGenResetAt: new Date(now + 24 * 60 * 60 * 1000) },
118
- });
119
- } else if (admin.keysGenToday >= admin.keysGenLimit) {
120
- const resetAtTime = admin.keysGenResetAt ? new Date(admin.keysGenResetAt).toLocaleTimeString() : 'midnight';
121
- return NextResponse.json({
122
- error: `Daily key limit reached (${admin.keysGenLimit}/${admin.keysGenLimit}). Resets at ${resetAtTime}.`
123
- }, { status: 429 });
124
- }
125
- }
126
- }
127
-
128
- const { key: rawKey, prefix } = generateApiKey();
129
- const effectiveTier = plan?.tier ?? 'free';
130
- const multiplier = plan?.displayMultiplier ?? 3.0;
131
- const effectiveLimit = Math.floor(Number(plan?.tokensPerWindow ?? 500_000) * multiplier);
132
- const now = Date.now();
133
-
134
- // Calculate expiration
135
- let expiresAt: Date | null = null;
136
- if (plan && plan.durationDays > 0) {
137
- let durationDays = days !== undefined ? days : plan.durationDays;
138
- // Superadmin/admin can override duration up to 365 days
139
- const maxAllowed = me.role === 'super_admin' || me.role === 'admin' ? 365 : (plan.maxDurationDays === -1 ? durationDays : plan.maxDurationDays);
140
- durationDays = Math.min(durationDays, maxAllowed);
141
- const minDays = plan.minDurationDays;
142
- durationDays = Math.max(minDays, durationDays);
143
- expiresAt = new Date(now + durationDays * 24 * 60 * 60 * 1000);
144
- }
145
-
146
- // Superadmin/admin can set custom token limit override
147
- const effectiveTokenLimit = tokenLimitOverride && (me.role === 'super_admin' || me.role === 'admin')
148
- ? Number(tokenLimitOverride)
149
- : undefined;
150
-
151
- const apiKey = await prisma.apiKey.create({
152
- data: {
153
- key: rawKey,
154
- prefix,
155
- name: name ?? 'ClaudMax Key',
156
- tier: effectiveTier,
157
- resellerId: me.role === 'reseller' ? me.id : (resellerId ?? null),
158
- displayMultiplier: multiplier,
159
- windowStartAt: new Date(now),
160
- planId: effectivePlanId ?? null,
161
- expiresAt,
162
- tokenLimitOverride: effectiveTokenLimit ?? null,
163
- },
164
- });
165
-
166
- // Increment reseller's key count
167
- if (me.role === 'reseller') {
168
- await prisma.admin.update({
169
- where: { id: me.id },
170
- data: { keysGenToday: { increment: 1 } },
171
- });
172
- }
173
-
174
- return NextResponse.json({
175
- key: apiKey.key,
176
- id: apiKey.id,
177
- name: apiKey.name,
178
- prefix: apiKey.prefix,
179
- tier: apiKey.tier,
180
- isActive: apiKey.isActive,
181
- displayMultiplier: apiKey.displayMultiplier,
182
- planId: apiKey.planId,
183
- expiresAt: expiresAt?.toISOString() ?? null,
184
- requestsUsed: 0,
185
- requestsLimit: plan?.requestsPerWindow ?? 100,
186
- tokensUsed: 0,
187
- tokensLimit: effectiveLimit,
188
- windowResetAt: new Date(now + WINDOW_MS).toISOString(),
189
- createdAt: apiKey.createdAt.toISOString(),
190
- lastUsedAt: null,
191
- }, { status: 201 });
192
- }
@@ -1,81 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { prisma } from '@/lib/prisma';
3
- import { getAuthUser } from '@/lib/auth';
4
-
5
- export async function GET(req: Request) {
6
- const me = await getAuthUser();
7
- if (!me) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
8
-
9
- const { searchParams } = new URL(req.url);
10
- const page = parseInt(searchParams.get('page') ?? '1');
11
- const limit = parseInt(searchParams.get('limit') ?? '15');
12
- const search = searchParams.get('search') ?? '';
13
- const tier = searchParams.get('tier') ?? '';
14
- const status = searchParams.get('status') ?? '';
15
- const skip = (page - 1) * limit;
16
-
17
- // Role-based key filter
18
- const where: any = {};
19
- if (me.role === 'reseller') {
20
- where.resellerId = me.id;
21
- } else if (me.role === 'admin') {
22
- where.resellerId = { not: null };
23
- }
24
-
25
- if (search) {
26
- where.OR = [
27
- { name: { contains: search } },
28
- { key: { contains: search } },
29
- { prefix: { contains: search } },
30
- ];
31
- }
32
- if (tier) where.tier = tier;
33
- if (status === 'active') where.isActive = true;
34
- if (status === 'inactive') where.isActive = false;
35
-
36
- const [keys, total] = await Promise.all([
37
- prisma.apiKey.findMany({
38
- where,
39
- orderBy: { createdAt: 'desc' },
40
- skip,
41
- take: limit,
42
- select: {
43
- id: true, key: true, name: true, prefix: true, tier: true,
44
- isActive: true, displayMultiplier: true, blockedUntil: true,
45
- windowTokensUsed: true, windowRequestsUsed: true, totalTokensUsed: true,
46
- windowStartAt: true, lastUsedAt: true, resellerId: true, createdAt: true,
47
- tokenLimitOverride: true, planId: true, expiresAt: true,
48
- plan: { select: { id: true, name: true, tier: true, durationDays: true } },
49
- },
50
- }),
51
- prisma.apiKey.count({ where }),
52
- ]);
53
-
54
- // If reseller, show their own keys with their name as reseller
55
- const resellerIds = [...new Set(keys.map(k => k.resellerId).filter(Boolean))] as string[];
56
- const resellerMap: Record<string, { id: string; name: string; username: string }> = {};
57
- if (resellerIds.length > 0) {
58
- const resellers = await prisma.admin.findMany({
59
- where: { id: { in: resellerIds } },
60
- select: { id: true, name: true, username: true },
61
- });
62
- for (const r of resellers) resellerMap[r.id] = r;
63
- }
64
-
65
- return NextResponse.json({
66
- keys: keys.map(k => ({
67
- ...k,
68
- windowTokensUsed: k.windowTokensUsed,
69
- totalTokensUsed: k.totalTokensUsed,
70
- displayMultiplier: k.displayMultiplier ?? 3.0,
71
- tokenLimitOverride: k.tokenLimitOverride,
72
- blockedUntil: k.blockedUntil?.toISOString() ?? null,
73
- reseller: resellerMap[k.resellerId ?? ''] ?? null,
74
- expiresAt: k.expiresAt?.toISOString() ?? null,
75
- plan: k.plan ?? null,
76
- })),
77
- total,
78
- pages: Math.ceil(total / limit),
79
- page,
80
- });
81
- }