@chimerai/cli 0.2.73

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +317 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.d.ts.map +1 -0
  8. package/dist/commands/add.js +2126 -0
  9. package/dist/commands/create.d.ts +12 -0
  10. package/dist/commands/create.d.ts.map +1 -0
  11. package/dist/commands/create.js +1703 -0
  12. package/dist/commands/deploy.d.ts +11 -0
  13. package/dist/commands/deploy.d.ts.map +1 -0
  14. package/dist/commands/deploy.js +219 -0
  15. package/dist/commands/dev.d.ts +17 -0
  16. package/dist/commands/dev.d.ts.map +1 -0
  17. package/dist/commands/dev.js +206 -0
  18. package/dist/commands/doctor.d.ts +11 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +728 -0
  21. package/dist/commands/generate.d.ts +19 -0
  22. package/dist/commands/generate.d.ts.map +1 -0
  23. package/dist/commands/generate.js +429 -0
  24. package/dist/commands/init.d.ts +11 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +269 -0
  27. package/dist/commands/list.d.ts +12 -0
  28. package/dist/commands/list.d.ts.map +1 -0
  29. package/dist/commands/list.js +328 -0
  30. package/dist/commands/migrate.d.ts +14 -0
  31. package/dist/commands/migrate.d.ts.map +1 -0
  32. package/dist/commands/migrate.js +197 -0
  33. package/dist/commands/plugin.d.ts +10 -0
  34. package/dist/commands/plugin.d.ts.map +1 -0
  35. package/dist/commands/plugin.js +239 -0
  36. package/dist/commands/remove.d.ts +11 -0
  37. package/dist/commands/remove.d.ts.map +1 -0
  38. package/dist/commands/remove.js +472 -0
  39. package/dist/commands/secret.d.ts +12 -0
  40. package/dist/commands/secret.d.ts.map +1 -0
  41. package/dist/commands/secret.js +102 -0
  42. package/dist/commands/setup.d.ts +9 -0
  43. package/dist/commands/setup.d.ts.map +1 -0
  44. package/dist/commands/setup.js +788 -0
  45. package/dist/commands/update.d.ts +14 -0
  46. package/dist/commands/update.d.ts.map +1 -0
  47. package/dist/commands/update.js +211 -0
  48. package/dist/commands/use.d.ts +9 -0
  49. package/dist/commands/use.d.ts.map +1 -0
  50. package/dist/commands/use.js +51 -0
  51. package/dist/index.d.ts +22 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +45 -0
  54. package/dist/license.d.ts +55 -0
  55. package/dist/license.d.ts.map +1 -0
  56. package/dist/license.js +258 -0
  57. package/dist/scanner.d.ts +31 -0
  58. package/dist/scanner.d.ts.map +1 -0
  59. package/dist/scanner.js +113 -0
  60. package/dist/schema-manager.d.ts +26 -0
  61. package/dist/schema-manager.d.ts.map +1 -0
  62. package/dist/schema-manager.js +132 -0
  63. package/dist/templates/admin.d.ts +49 -0
  64. package/dist/templates/admin.d.ts.map +1 -0
  65. package/dist/templates/admin.js +1358 -0
  66. package/dist/templates/ai-routes.d.ts +17 -0
  67. package/dist/templates/ai-routes.d.ts.map +1 -0
  68. package/dist/templates/ai-routes.js +1130 -0
  69. package/dist/templates/ai-service-tools.d.ts +22 -0
  70. package/dist/templates/ai-service-tools.d.ts.map +1 -0
  71. package/dist/templates/ai-service-tools.js +1424 -0
  72. package/dist/templates/ai-service.d.ts +66 -0
  73. package/dist/templates/ai-service.d.ts.map +1 -0
  74. package/dist/templates/ai-service.js +2202 -0
  75. package/dist/templates/api-routes.d.ts +108 -0
  76. package/dist/templates/api-routes.d.ts.map +1 -0
  77. package/dist/templates/api-routes.js +1219 -0
  78. package/dist/templates/auth.d.ts +48 -0
  79. package/dist/templates/auth.d.ts.map +1 -0
  80. package/dist/templates/auth.js +381 -0
  81. package/dist/templates/billing.d.ts +44 -0
  82. package/dist/templates/billing.d.ts.map +1 -0
  83. package/dist/templates/billing.js +551 -0
  84. package/dist/templates/chat.d.ts +63 -0
  85. package/dist/templates/chat.d.ts.map +1 -0
  86. package/dist/templates/chat.js +1979 -0
  87. package/dist/templates/components.d.ts +22 -0
  88. package/dist/templates/components.d.ts.map +1 -0
  89. package/dist/templates/components.js +672 -0
  90. package/dist/templates/config.d.ts +6 -0
  91. package/dist/templates/config.d.ts.map +1 -0
  92. package/dist/templates/config.js +86 -0
  93. package/dist/templates/docker.d.ts +25 -0
  94. package/dist/templates/docker.d.ts.map +1 -0
  95. package/dist/templates/docker.js +165 -0
  96. package/dist/templates/gdpr.d.ts +16 -0
  97. package/dist/templates/gdpr.d.ts.map +1 -0
  98. package/dist/templates/gdpr.js +259 -0
  99. package/dist/templates/index.d.ts +77 -0
  100. package/dist/templates/index.d.ts.map +1 -0
  101. package/dist/templates/index.js +339 -0
  102. package/dist/templates/layout.d.ts +67 -0
  103. package/dist/templates/layout.d.ts.map +1 -0
  104. package/dist/templates/layout.js +670 -0
  105. package/dist/templates/mfa.d.ts +23 -0
  106. package/dist/templates/mfa.d.ts.map +1 -0
  107. package/dist/templates/mfa.js +353 -0
  108. package/dist/templates/middleware.d.ts +12 -0
  109. package/dist/templates/middleware.d.ts.map +1 -0
  110. package/dist/templates/middleware.js +116 -0
  111. package/dist/templates/prisma.d.ts +35 -0
  112. package/dist/templates/prisma.d.ts.map +1 -0
  113. package/dist/templates/prisma.js +724 -0
  114. package/dist/templates/provider-routes.d.ts +21 -0
  115. package/dist/templates/provider-routes.d.ts.map +1 -0
  116. package/dist/templates/provider-routes.js +1203 -0
  117. package/dist/templates/rag.d.ts +48 -0
  118. package/dist/templates/rag.d.ts.map +1 -0
  119. package/dist/templates/rag.js +532 -0
  120. package/dist/templates/widget.d.ts +64 -0
  121. package/dist/templates/widget.d.ts.map +1 -0
  122. package/dist/templates/widget.js +1360 -0
  123. package/dist/utils/provider-db.d.ts +63 -0
  124. package/dist/utils/provider-db.d.ts.map +1 -0
  125. package/dist/utils/provider-db.js +300 -0
  126. package/dist/utils.d.ts +78 -0
  127. package/dist/utils.d.ts.map +1 -0
  128. package/dist/utils.js +330 -0
  129. package/package.json +60 -0
@@ -0,0 +1,1358 @@
1
+ "use strict";
2
+ /**
3
+ * Admin panel component templates
4
+ * Generates admin dashboard, users management page, roles management page, and admin layout guard
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.generateAdminLayout = generateAdminLayout;
8
+ exports.generateAdminDashboardPage = generateAdminDashboardPage;
9
+ exports.generateAdminUsersPage = generateAdminUsersPage;
10
+ exports.generateAdminRolesPage = generateAdminRolesPage;
11
+ exports.generateAdminSettingsPage = generateAdminSettingsPage;
12
+ exports.generateAdminLogsPage = generateAdminLogsPage;
13
+ exports.generatePermissionsLib = generatePermissionsLib;
14
+ exports.generateRequirePermissionLib = generateRequirePermissionLib;
15
+ exports.generateAuditLogHelper = generateAuditLogHelper;
16
+ /**
17
+ * Generates the admin layout with session check and admin role guard.
18
+ * Redirects unauthenticated users to sign-in, unauthorized users to dashboard.
19
+ * @returns TypeScript/JSX content for app/admin/layout.tsx
20
+ */
21
+ function generateAdminLayout() {
22
+ return `// @chimerai component=AdminLayout version=1.1
23
+ 'use client';
24
+
25
+ import { useSession, signOut } from 'next-auth/react';
26
+ import { redirect } from 'next/navigation';
27
+ import { useEffect, useState } from 'react';
28
+ import Link from 'next/link';
29
+ import { useAppName } from '@/lib/use-app-name';
30
+
31
+ export default function AdminLayout({
32
+ children,
33
+ }: {
34
+ children: React.ReactNode;
35
+ }) {
36
+ const { data: session, status } = useSession();
37
+ const [isAuthorized, setIsAuthorized] = useState(false);
38
+ const [isLoading, setIsLoading] = useState(true);
39
+ const appName = useAppName();
40
+
41
+ useEffect(() => {
42
+ if (status === 'loading') return;
43
+
44
+ if (!session) {
45
+ redirect('/auth/signin');
46
+ return;
47
+ }
48
+
49
+ // Check if user has admin role
50
+ const userRoles = (session.user as any)?.roles || [];
51
+ const hasAdmin = userRoles.some(
52
+ (r: any) => r.name === 'admin' || r.name === 'Admin'
53
+ );
54
+
55
+ if (!hasAdmin) {
56
+ redirect('/dashboard');
57
+ return;
58
+ }
59
+
60
+ setIsAuthorized(true);
61
+ setIsLoading(false);
62
+ }, [session, status]);
63
+
64
+ useEffect(() => {
65
+ document.title = \`Admin — \${appName}\`;
66
+ }, [appName]);
67
+
68
+ if (isLoading || status === 'loading') {
69
+ return (
70
+ <div className="flex min-h-screen items-center justify-center">
71
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ if (!isAuthorized) {
77
+ return null;
78
+ }
79
+
80
+ return (
81
+ <div className="flex min-h-screen bg-white dark:bg-gray-900">
82
+ <aside className="w-64 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 p-4 flex flex-col">
83
+ <div className="mb-4">
84
+ <h2 className="text-lg font-bold dark:text-white">{appName}</h2>
85
+ <p className="text-xs text-gray-500 dark:text-gray-400">Admin Panel</p>
86
+ </div>
87
+ <nav className="space-y-2 flex-1">
88
+ <Link href="/admin" className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">
89
+ 📊 Dashboard
90
+ </Link>
91
+ <Link href="/admin/users" className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">
92
+ 👥 Users
93
+ </Link>
94
+ <Link href="/admin/roles" className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">
95
+ 🔑 Roles
96
+ </Link>
97
+ <Link href="/admin/settings" className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">
98
+ ⚙️ Settings
99
+ </Link>
100
+ <Link href="/admin/logs" className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">
101
+ 📋 Logs
102
+ </Link>
103
+ <hr className="my-2 border-gray-200 dark:border-gray-700" />
104
+ <Link href="/dashboard" className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">
105
+ ← Back to Dashboard
106
+ </Link>
107
+ </nav>
108
+ <div className="border-t border-gray-200 dark:border-gray-700 pt-3 mt-3">
109
+ <div className="px-4 py-1 text-xs text-gray-500 dark:text-gray-400 truncate mb-1">
110
+ {session?.user?.email}
111
+ </div>
112
+ <button
113
+ onClick={() => signOut({ callbackUrl: '/auth/signin' })}
114
+ className="w-full flex items-center gap-2 px-4 py-2 rounded text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
115
+ >
116
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
117
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
118
+ </svg>
119
+ Sign out
120
+ </button>
121
+ </div>
122
+ </aside>
123
+ <main className="flex-1 p-8 dark:text-white">
124
+ {children}
125
+ </main>
126
+ </div>
127
+ );
128
+ }
129
+ `;
130
+ }
131
+ /**
132
+ * Generates the main admin dashboard
133
+ * Shows cards linking to RBAC, AI configuration, and system management sections
134
+ * @returns TypeScript/JSX content for app/admin/page.tsx
135
+ */
136
+ function generateAdminDashboardPage() {
137
+ return `// @chimerai component=AdminDashboardPage version=1.1
138
+ 'use client';
139
+
140
+ import Link from 'next/link';
141
+
142
+ export default function AdminDashboard() {
143
+ return (
144
+ <div className="container mx-auto py-8">
145
+ <div className="mb-8">
146
+ <h1 className="text-4xl font-bold mb-2 dark:text-white">Admin Dashboard</h1>
147
+ <p className="text-gray-600 dark:text-gray-400">Manage your ChimerAI system</p>
148
+ </div>
149
+
150
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
151
+ {/* RBAC Management */}
152
+ <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
153
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2 dark:text-white">
154
+ <span className="text-2xl">🔐</span> RBAC Management
155
+ </h2>
156
+ <p className="text-gray-600 dark:text-gray-400 mb-4">Manage users, roles, and permissions</p>
157
+ <div className="space-y-2">
158
+ <Link href="/admin/users" className="block px-4 py-2 bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-100 dark:hover:bg-blue-900/50">
159
+ 👥 Users
160
+ </Link>
161
+ <Link href="/admin/roles" className="block px-4 py-2 bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-100 dark:hover:bg-blue-900/50">
162
+ 🔑 Roles
163
+ </Link>
164
+ </div>
165
+ </div>
166
+
167
+ {/* AI Configuration */}
168
+ <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
169
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2 dark:text-white">
170
+ <span className="text-2xl">🤖</span> AI Configuration
171
+ </h2>
172
+ <p className="text-gray-600 dark:text-gray-400 mb-4">Configure AI models and providers</p>
173
+ <div className="space-y-2">
174
+ <Link href="/dashboard/providers" className="block px-4 py-2 bg-purple-50 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded hover:bg-purple-100 dark:hover:bg-purple-900/50">
175
+ 🔌 Model Providers
176
+ </Link>
177
+ <Link href="/dashboard/prompts" className="block px-4 py-2 bg-purple-50 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded hover:bg-purple-100 dark:hover:bg-purple-900/50">
178
+ 📝 Prompt Templates
179
+ </Link>
180
+ </div>
181
+ </div>
182
+
183
+ {/* System */}
184
+ <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
185
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2 dark:text-white">
186
+ <span className="text-2xl">⚙️</span> System
187
+ </h2>
188
+ <p className="text-gray-600 dark:text-gray-400 mb-4">System configuration and monitoring</p>
189
+ <div className="space-y-2">
190
+ <Link href="/admin/settings" className="block px-4 py-2 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
191
+ ⚙️ Settings
192
+ </Link>
193
+ <Link href="/admin/logs" className="block px-4 py-2 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
194
+ 📋 Activity Logs
195
+ </Link>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+ `;
203
+ }
204
+ /**
205
+ * Generates the admin users management page
206
+ * Displays user list with search, create/edit/delete functionality
207
+ * @returns TypeScript/JSX content for app/admin/users/page.tsx
208
+ */
209
+ function generateAdminUsersPage() {
210
+ return `// @chimerai component=AdminUsersPage version=1.0
211
+ // @ts-nocheck
212
+ 'use client';
213
+
214
+ import { useState, useEffect } from 'react';
215
+
216
+ export default function UsersPage() {
217
+ const [users, setUsers] = useState([]);
218
+ const [roles, setRoles] = useState<any[]>([]);
219
+ const [loading, setLoading] = useState(true);
220
+ const [searchTerm, setSearchTerm] = useState('');
221
+ const [showCreateModal, setShowCreateModal] = useState(false);
222
+ const [showEditModal, setShowEditModal] = useState(false);
223
+ const [newUser, setNewUser] = useState({ email: '', name: '', password: '', roleIds: [] as string[] });
224
+ const [editUser, setEditUser] = useState({ id: '', email: '', name: '', roleIds: [] as string[] });
225
+
226
+ useEffect(() => {
227
+ fetchUsers();
228
+ fetchRoles();
229
+ }, []);
230
+
231
+ async function fetchRoles() {
232
+ try {
233
+ const res = await fetch('/api/admin/roles');
234
+ const data = await res.json();
235
+ setRoles(Array.isArray(data) ? data : []);
236
+ } catch (error) {
237
+ console.error('Failed to fetch roles:', error);
238
+ }
239
+ }
240
+
241
+ async function fetchUsers() {
242
+ try {
243
+ const res = await fetch('/api/admin/users');
244
+ const data = await res.json();
245
+ setUsers(Array.isArray(data) ? data : []);
246
+ } catch (error) {
247
+ console.error('Failed to fetch users:', error);
248
+ setUsers([]);
249
+ } finally {
250
+ setLoading(false);
251
+ }
252
+ }
253
+
254
+ async function handleDelete(userId: string) {
255
+ if (!confirm('Are you sure?')) return;
256
+
257
+ try {
258
+ await fetch(\`/api/admin/users/\${userId}\`, { method: 'DELETE' });
259
+ await fetchUsers();
260
+ } catch (error) {
261
+ console.error('Failed to delete user:', error);
262
+ }
263
+ }
264
+
265
+ async function handleCreateUser(e: React.FormEvent) {
266
+ e.preventDefault();
267
+ try {
268
+ const res = await fetch('/api/admin/users', {
269
+ method: 'POST',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify(newUser),
272
+ });
273
+
274
+ if (res.ok) {
275
+ setShowCreateModal(false);
276
+ setNewUser({ email: '', name: '', password: '', roleIds: [] });
277
+ await fetchUsers();
278
+ } else {
279
+ alert('Failed to create user');
280
+ }
281
+ } catch (error) {
282
+ console.error('Failed to create user:', error);
283
+ alert('Failed to create user');
284
+ }
285
+ }
286
+
287
+ async function handleEditUser(e: React.FormEvent) {
288
+ e.preventDefault();
289
+ try {
290
+ const res = await fetch(\`/api/admin/users/\${editUser.id}\`, {
291
+ method: 'PUT',
292
+ headers: { 'Content-Type': 'application/json' },
293
+ body: JSON.stringify({ email: editUser.email, name: editUser.name, roleIds: editUser.roleIds }),
294
+ });
295
+
296
+ if (res.ok) {
297
+ setShowEditModal(false);
298
+ setEditUser({ id: '', email: '', name: '', roleIds: [] });
299
+ await fetchUsers();
300
+ } else {
301
+ alert('Failed to update user');
302
+ }
303
+ } catch (error) {
304
+ console.error('Failed to update user:', error);
305
+ alert('Failed to update user');
306
+ }
307
+ }
308
+
309
+ function openEditModal(user: any) {
310
+ setEditUser({
311
+ id: user.id,
312
+ email: user.email,
313
+ name: user.name || '',
314
+ roleIds: (user.roles || []).map((r: any) => r.id),
315
+ });
316
+ setShowEditModal(true);
317
+ }
318
+
319
+ const filteredUsers = users.filter(
320
+ (user: any) =>
321
+ user.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
322
+ user.name?.toLowerCase().includes(searchTerm.toLowerCase())
323
+ );
324
+
325
+ if (loading) {
326
+ return <div className="container mx-auto py-8 bg-white min-h-screen">Loading...</div>;
327
+ }
328
+
329
+ return (
330
+ <div className="container mx-auto py-8">
331
+ <div className="flex justify-between items-center mb-6">
332
+ <h1 className="text-3xl font-bold dark:text-white">Users</h1>
333
+ <button
334
+ onClick={() => setShowCreateModal(true)}
335
+ className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
336
+ >
337
+ Add User
338
+ </button>
339
+ </div>
340
+
341
+ <div className="mb-4">
342
+ <input
343
+ type="text"
344
+ placeholder="Search users..."
345
+ value={searchTerm}
346
+ onChange={(e) => setSearchTerm(e.target.value)}
347
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white"
348
+ />
349
+ </div>
350
+
351
+ <div className="overflow-x-auto bg-white dark:bg-gray-800 rounded-lg shadow">
352
+ <table className="w-full">
353
+ <thead className="bg-gray-50 dark:bg-gray-700 border-b dark:border-gray-600">
354
+ <tr>
355
+ <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Email</th>
356
+ <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Name</th>
357
+ <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Roles</th>
358
+ <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Created</th>
359
+ <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Actions</th>
360
+ </tr>
361
+ </thead>
362
+ <tbody>
363
+ {filteredUsers.map((user: any) => (
364
+ <tr key={user.id} className="border-b dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700">
365
+ <td className="px-6 py-4 text-sm dark:text-gray-300">{user.email}</td>
366
+ <td className="px-6 py-4 text-sm dark:text-gray-300">{user.name}</td>
367
+ <td className="px-6 py-4 text-sm">
368
+ <div className="flex flex-wrap gap-1">
369
+ {(user.roles || []).map((role: any) => (
370
+ <span key={role.id} className="px-2 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 text-xs rounded-full">
371
+ {role.name}
372
+ </span>
373
+ ))}
374
+ </div>
375
+ </td>
376
+ <td className="px-6 py-4 text-sm dark:text-gray-300">{new Date(user.createdAt).toLocaleDateString()}</td>
377
+ <td className="px-6 py-4 text-sm space-x-2">
378
+ <button
379
+ onClick={() => openEditModal(user)}
380
+ className="px-3 py-1 bg-yellow-50 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 rounded hover:bg-yellow-100 dark:hover:bg-yellow-900/50"
381
+ >
382
+ Edit
383
+ </button>
384
+ <button
385
+ onClick={() => handleDelete(user.id)}
386
+ className="px-3 py-1 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded hover:bg-red-100 dark:hover:bg-red-900/50"
387
+ >
388
+ Delete
389
+ </button>
390
+ </td>
391
+ </tr>
392
+ ))}
393
+ </tbody>
394
+ </table>
395
+ </div>
396
+
397
+ {/* Create Modal */}
398
+ {showCreateModal && (
399
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
400
+ <div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full">
401
+ <h2 className="text-2xl font-bold mb-4 dark:text-white">Create User</h2>
402
+ <form onSubmit={handleCreateUser}>
403
+ <input
404
+ type="email"
405
+ placeholder="Email"
406
+ value={newUser.email}
407
+ onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
408
+ required
409
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded mb-4 dark:bg-gray-700 dark:text-white"
410
+ />
411
+ <input
412
+ type="text"
413
+ placeholder="Name"
414
+ value={newUser.name}
415
+ onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
416
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded mb-4 dark:bg-gray-700 dark:text-white"
417
+ />
418
+ <input
419
+ type="password"
420
+ placeholder="Password"
421
+ value={newUser.password}
422
+ onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
423
+ required
424
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded mb-4 dark:bg-gray-700 dark:text-white"
425
+ />
426
+ {roles.length > 0 && (
427
+ <div className="mb-4">
428
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Roles</label>
429
+ <div className="space-y-2 max-h-40 overflow-y-auto">
430
+ {roles.map((role: any) => (
431
+ <label key={role.id} className="flex items-center gap-2">
432
+ <input
433
+ type="checkbox"
434
+ checked={newUser.roleIds.includes(role.id)}
435
+ onChange={(e) => {
436
+ const ids = e.target.checked
437
+ ? [...newUser.roleIds, role.id]
438
+ : newUser.roleIds.filter((id: string) => id !== role.id);
439
+ setNewUser({ ...newUser, roleIds: ids });
440
+ }}
441
+ className="rounded border-gray-300 dark:border-gray-600"
442
+ />
443
+ <span className="text-sm dark:text-gray-300">{role.name}</span>
444
+ </label>
445
+ ))}
446
+ </div>
447
+ </div>
448
+ )}
449
+ <div className="flex gap-2">
450
+ <button
451
+ type="submit"
452
+ className="flex-1 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
453
+ >
454
+ Create
455
+ </button>
456
+ <button
457
+ type="button"
458
+ onClick={() => setShowCreateModal(false)}
459
+ className="flex-1 px-4 py-2 bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 dark:hover:bg-gray-500"
460
+ >
461
+ Cancel
462
+ </button>
463
+ </div>
464
+ </form>
465
+ </div>
466
+ </div>
467
+ )}
468
+
469
+ {/* Edit Modal */}
470
+ {showEditModal && (
471
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
472
+ <div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full">
473
+ <h2 className="text-2xl font-bold mb-4 dark:text-white">Edit User</h2>
474
+ <form onSubmit={handleEditUser}>
475
+ <input
476
+ type="email"
477
+ placeholder="Email"
478
+ value={editUser.email}
479
+ onChange={(e) => setEditUser({ ...editUser, email: e.target.value })}
480
+ required
481
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded mb-4 dark:bg-gray-700 dark:text-white"
482
+ />
483
+ <input
484
+ type="text"
485
+ placeholder="Name"
486
+ value={editUser.name}
487
+ onChange={(e) => setEditUser({ ...editUser, name: e.target.value })}
488
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded mb-4 dark:bg-gray-700 dark:text-white"
489
+ />
490
+ {roles.length > 0 && (
491
+ <div className="mb-4">
492
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Roles</label>
493
+ <div className="space-y-2 max-h-40 overflow-y-auto">
494
+ {roles.map((role: any) => (
495
+ <label key={role.id} className="flex items-center gap-2">
496
+ <input
497
+ type="checkbox"
498
+ checked={editUser.roleIds.includes(role.id)}
499
+ onChange={(e) => {
500
+ const ids = e.target.checked
501
+ ? [...editUser.roleIds, role.id]
502
+ : editUser.roleIds.filter((id: string) => id !== role.id);
503
+ setEditUser({ ...editUser, roleIds: ids });
504
+ }}
505
+ className="rounded border-gray-300 dark:border-gray-600"
506
+ />
507
+ <span className="text-sm dark:text-gray-300">{role.name}</span>
508
+ </label>
509
+ ))}
510
+ </div>
511
+ </div>
512
+ )}
513
+ <div className="flex gap-2">
514
+ <button
515
+ type="submit"
516
+ className="flex-1 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
517
+ >
518
+ Update
519
+ </button>
520
+ <button
521
+ type="button"
522
+ onClick={() => setShowEditModal(false)}
523
+ className="flex-1 px-4 py-2 bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 dark:hover:bg-gray-500"
524
+ >
525
+ Cancel
526
+ </button>
527
+ </div>
528
+ </form>
529
+ </div>
530
+ </div>
531
+ )}
532
+ </div>
533
+ );
534
+ }
535
+ `;
536
+ }
537
+ /**
538
+ * Generates the admin roles management page
539
+ * Allows CRUD operations on roles and permission assignment
540
+ * @returns TypeScript/JSX content for app/admin/roles/page.tsx
541
+ */
542
+ function generateAdminRolesPage() {
543
+ return `// @chimerai component=AdminRolesPage version=1.0
544
+ // @ts-nocheck
545
+ 'use client';
546
+
547
+ import { useState, useEffect } from 'react';
548
+
549
+ const AVAILABLE_PERMISSIONS = [
550
+ { id: 'users:read', label: 'Users: Read', description: 'View user list' },
551
+ { id: 'users:write', label: 'Users: Write', description: 'Create/edit users' },
552
+ { id: 'users:delete', label: 'Users: Delete', description: 'Delete users' },
553
+ { id: 'roles:read', label: 'Roles: Read', description: 'View role list' },
554
+ { id: 'roles:write', label: 'Roles: Write', description: 'Create/edit roles' },
555
+ { id: 'roles:delete', label: 'Roles: Delete', description: 'Delete roles' },
556
+ { id: 'settings:read', label: 'Settings: Read', description: 'View settings' },
557
+ { id: 'settings:write', label: 'Settings: Write', description: 'Modify settings' },
558
+ { id: 'admin:*', label: 'Admin: Full Access', description: 'Full admin access' },
559
+ ];
560
+
561
+ export default function RolesPage() {
562
+ const [roles, setRoles] = useState([]);
563
+ const [loading, setLoading] = useState(true);
564
+ const [searchTerm, setSearchTerm] = useState('');
565
+ const [showCreateModal, setShowCreateModal] = useState(false);
566
+ const [showEditModal, setShowEditModal] = useState(false);
567
+ const [newRole, setNewRole] = useState({ name: '', description: '', permissions: [] as string[] });
568
+ const [editRole, setEditRole] = useState({ id: '', name: '', description: '', permissions: [] as string[] });
569
+
570
+ useEffect(() => {
571
+ fetchRoles();
572
+ }, []);
573
+
574
+ async function fetchRoles() {
575
+ try {
576
+ const res = await fetch('/api/admin/roles');
577
+ const data = await res.json();
578
+ setRoles(Array.isArray(data) ? data : []);
579
+ } catch (error) {
580
+ console.error('Failed to fetch roles:', error);
581
+ setRoles([]);
582
+ } finally {
583
+ setLoading(false);
584
+ }
585
+ }
586
+
587
+ async function handleDelete(roleId: string) {
588
+ if (!confirm('Are you sure you want to delete this role?')) return;
589
+
590
+ try {
591
+ await fetch(\`/api/admin/roles/\${roleId}\`, { method: 'DELETE' });
592
+ await fetchRoles();
593
+ } catch (error) {
594
+ console.error('Failed to delete role:', error);
595
+ alert('Failed to delete role');
596
+ }
597
+ }
598
+
599
+ async function handleCreateRole(e: React.FormEvent) {
600
+ e.preventDefault();
601
+ try {
602
+ const res = await fetch('/api/admin/roles', {
603
+ method: 'POST',
604
+ headers: { 'Content-Type': 'application/json' },
605
+ body: JSON.stringify({
606
+ name: newRole.name,
607
+ description: newRole.description,
608
+ permissions: newRole.permissions
609
+ })
610
+ });
611
+
612
+ if (res.ok) {
613
+ setShowCreateModal(false);
614
+ setNewRole({ name: '', description: '', permissions: [] });
615
+ await fetchRoles();
616
+ } else {
617
+ alert('Failed to create role');
618
+ }
619
+ } catch (error) {
620
+ console.error('Failed to create role:', error);
621
+ alert('Failed to create role');
622
+ }
623
+ }
624
+
625
+ async function handleEditRole(e: React.FormEvent) {
626
+ e.preventDefault();
627
+ try {
628
+ const res = await fetch(\`/api/admin/roles/\${editRole.id}\`, {
629
+ method: 'PUT',
630
+ headers: { 'Content-Type': 'application/json' },
631
+ body: JSON.stringify({
632
+ name: editRole.name,
633
+ description: editRole.description,
634
+ permissions: editRole.permissions
635
+ })
636
+ });
637
+
638
+ if (res.ok) {
639
+ setShowEditModal(false);
640
+ setEditRole({ id: '', name: '', description: '', permissions: [] });
641
+ await fetchRoles();
642
+ } else {
643
+ alert('Failed to update role');
644
+ }
645
+ } catch (error) {
646
+ console.error('Failed to update role:', error);
647
+ alert('Failed to update role');
648
+ }
649
+ }
650
+
651
+ function openEditModal(role: any) {
652
+ setEditRole({
653
+ id: role.id,
654
+ name: role.name,
655
+ description: role.description || '',
656
+ permissions: role.permissions || []
657
+ });
658
+ setShowEditModal(true);
659
+ }
660
+
661
+ const filteredRoles = roles.filter((role: any) =>
662
+ role.name?.toLowerCase().includes(searchTerm.toLowerCase())
663
+ );
664
+
665
+ if (loading) {
666
+ return <div className="container mx-auto py-8 dark:text-white">Loading...</div>;
667
+ }
668
+
669
+ return (
670
+ <div className="container mx-auto py-8">
671
+ <div className="flex justify-between items-center mb-6">
672
+ <h1 className="text-3xl font-bold dark:text-white">Roles</h1>
673
+ <button
674
+ onClick={() => setShowCreateModal(true)}
675
+ className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
676
+ >
677
+ Add Role
678
+ </button>
679
+ </div>
680
+
681
+ <div className="mb-4">
682
+ <input
683
+ type="text"
684
+ placeholder="Search roles..."
685
+ value={searchTerm}
686
+ onChange={(e) => setSearchTerm(e.target.value)}
687
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white"
688
+ />
689
+ </div>
690
+
691
+ <div className="grid gap-4">
692
+ {filteredRoles.map((role: any) => (
693
+ <div key={role.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
694
+ <div className="flex justify-between items-start mb-3">
695
+ <div>
696
+ <h3 className="text-lg font-semibold dark:text-white">{role.name}</h3>
697
+ <p className="text-gray-600 dark:text-gray-400 text-sm">{role.description}</p>
698
+ </div>
699
+ <div className="flex gap-2">
700
+ <button
701
+ onClick={() => openEditModal(role)}
702
+ className="px-3 py-1 bg-yellow-50 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 rounded hover:bg-yellow-100 dark:hover:bg-yellow-900/50"
703
+ >
704
+ Edit
705
+ </button>
706
+ <button
707
+ onClick={() => handleDelete(role.id)}
708
+ className="px-3 py-1 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded hover:bg-red-100 dark:hover:bg-red-900/50"
709
+ >
710
+ Delete
711
+ </button>
712
+ </div>
713
+ </div>
714
+ <div className="flex flex-wrap gap-2">
715
+ {role.permissions?.map((perm: string) => (
716
+ <span key={perm} className="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded text-xs">
717
+ {perm}
718
+ </span>
719
+ ))}
720
+ </div>
721
+ </div>
722
+ ))}
723
+ </div>
724
+
725
+ {/* Create/Edit Modal */}
726
+ {(showCreateModal || showEditModal) && (
727
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
728
+ <div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-2xl w-full max-h-screen overflow-y-auto">
729
+ <h2 className="text-2xl font-bold mb-4 dark:text-white">
730
+ {showCreateModal ? 'Create Role' : 'Edit Role'}
731
+ </h2>
732
+ <form onSubmit={showCreateModal ? handleCreateRole : handleEditRole}>
733
+ <input
734
+ type="text"
735
+ placeholder="Role Name"
736
+ value={showCreateModal ? newRole.name : editRole.name}
737
+ onChange={(e) =>
738
+ showCreateModal
739
+ ? setNewRole({ ...newRole, name: e.target.value })
740
+ : setEditRole({ ...editRole, name: e.target.value })
741
+ }
742
+ required
743
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded mb-4 dark:bg-gray-700 dark:text-white"
744
+ />
745
+ <textarea
746
+ placeholder="Description"
747
+ value={showCreateModal ? newRole.description : editRole.description}
748
+ onChange={(e) =>
749
+ showCreateModal
750
+ ? setNewRole({ ...newRole, description: e.target.value })
751
+ : setEditRole({ ...editRole, description: e.target.value })
752
+ }
753
+ className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded mb-4 dark:bg-gray-700 dark:text-white"
754
+ />
755
+ <div className="mb-4">
756
+ <label className="block text-sm font-semibold mb-2 dark:text-white">Permissions</label>
757
+ <div className="space-y-2 max-h-40 overflow-y-auto border border-gray-300 dark:border-gray-600 rounded p-4">
758
+ {AVAILABLE_PERMISSIONS.map((perm) => (
759
+ <label key={perm.id} className="flex items-center">
760
+ <input
761
+ type="checkbox"
762
+ checked={(
763
+ showCreateModal ? newRole.permissions : editRole.permissions
764
+ ).includes(perm.id)}
765
+ onChange={(e) => {
766
+ const permissions = showCreateModal ? newRole.permissions : editRole.permissions;
767
+ const updated = e.target.checked
768
+ ? [...permissions, perm.id]
769
+ : permissions.filter((p) => p !== perm.id);
770
+
771
+ if (showCreateModal) {
772
+ setNewRole({ ...newRole, permissions: updated });
773
+ } else {
774
+ setEditRole({ ...editRole, permissions: updated });
775
+ }
776
+ }}
777
+ className="mr-2"
778
+ />
779
+ <div>
780
+ <div className="text-sm font-medium dark:text-gray-300">{perm.label}</div>
781
+ <div className="text-xs text-gray-600 dark:text-gray-400">{perm.description}</div>
782
+ </div>
783
+ </label>
784
+ ))}
785
+ </div>
786
+ </div>
787
+ <div className="flex gap-2">
788
+ <button
789
+ type="submit"
790
+ className="flex-1 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
791
+ >
792
+ {showCreateModal ? 'Create' : 'Update'}
793
+ </button>
794
+ <button
795
+ type="button"
796
+ onClick={() => {
797
+ setShowCreateModal(false);
798
+ setShowEditModal(false);
799
+ }}
800
+ className="flex-1 px-4 py-2 bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 dark:hover:bg-gray-500"
801
+ >
802
+ Cancel
803
+ </button>
804
+ </div>
805
+ </form>
806
+ </div>
807
+ </div>
808
+ )}
809
+ </div>
810
+ );
811
+ }
812
+ `;
813
+ }
814
+ /**
815
+ * Generates the admin settings page
816
+ * Functional page for system configuration with save/reset
817
+ * @returns TypeScript/JSX content for app/admin/settings/page.tsx
818
+ */
819
+ function generateAdminSettingsPage() {
820
+ return `// @chimerai component=AdminSettingsPage version=2.0
821
+ 'use client';
822
+
823
+ import { useEffect, useState } from 'react';
824
+
825
+ interface Settings {
826
+ 'app.name': string;
827
+ 'app.language': string;
828
+ 'app.maintenanceMode': string;
829
+ }
830
+
831
+ export default function AdminSettingsPage() {
832
+ const [settings, setSettings] = useState<Settings>({
833
+ 'app.name': 'ChimerAI',
834
+ 'app.language': 'en',
835
+ 'app.maintenanceMode': 'false',
836
+ });
837
+ const [original, setOriginal] = useState<Settings | null>(null);
838
+ const [loading, setLoading] = useState(true);
839
+ const [saving, setSaving] = useState(false);
840
+ const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
841
+
842
+ useEffect(() => {
843
+ fetchSettings();
844
+ }, []);
845
+
846
+ async function fetchSettings() {
847
+ try {
848
+ const res = await fetch('/api/admin/settings');
849
+ if (res.ok) {
850
+ const data = await res.json();
851
+ setSettings(data);
852
+ setOriginal(data);
853
+ } else {
854
+ setOriginal({ ...settings });
855
+ }
856
+ } catch (err) {
857
+ console.error('Failed to load settings:', err);
858
+ setOriginal({ ...settings });
859
+ } finally {
860
+ setLoading(false);
861
+ }
862
+ }
863
+
864
+ const hasChanges = original && JSON.stringify(settings) !== JSON.stringify(original);
865
+
866
+ async function handleSave() {
867
+ setSaving(true);
868
+ setMessage(null);
869
+ try {
870
+ const res = await fetch('/api/admin/settings', {
871
+ method: 'PUT',
872
+ headers: { 'Content-Type': 'application/json' },
873
+ body: JSON.stringify(settings),
874
+ });
875
+ if (res.ok) {
876
+ setOriginal({ ...settings });
877
+ setMessage({ type: 'success', text: 'Settings saved successfully.' });
878
+ if (settings['app.name']) {
879
+ document.title = document.title.replace(/\u2014.*$/, \`\u2014 \${settings['app.name']}\`);
880
+ }
881
+ } else {
882
+ const data = await res.json();
883
+ setMessage({ type: 'error', text: data.error || 'Failed to save settings.' });
884
+ }
885
+ } catch (err) {
886
+ setMessage({ type: 'error', text: 'An error occurred while saving.' });
887
+ } finally {
888
+ setSaving(false);
889
+ }
890
+ }
891
+
892
+ function handleReset() {
893
+ if (original) {
894
+ setSettings({ ...original });
895
+ }
896
+ setMessage(null);
897
+ }
898
+
899
+ if (loading) {
900
+ return (
901
+ <div className="flex items-center justify-center py-20">
902
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
903
+ </div>
904
+ );
905
+ }
906
+
907
+ return (
908
+ <div className="container mx-auto py-8">
909
+ <div className="mb-8">
910
+ <h1 className="text-4xl font-bold mb-2 dark:text-white">System Settings</h1>
911
+ <p className="text-gray-600 dark:text-gray-400">Configure your ChimerAI system</p>
912
+ </div>
913
+
914
+ {message && (
915
+ <div className={\`mb-6 rounded-lg px-4 py-3 text-sm \${
916
+ message.type === 'success'
917
+ ? 'bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400'
918
+ : 'bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-400'
919
+ }\`}>
920
+ {message.text}
921
+ </div>
922
+ )}
923
+
924
+ <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-6">
925
+ <div className="border-b dark:border-gray-700 pb-4">
926
+ <h2 className="text-xl font-semibold mb-2 dark:text-white">General</h2>
927
+ <p className="text-gray-500 dark:text-gray-400 text-sm">General application settings</p>
928
+ </div>
929
+
930
+ <div className="space-y-4">
931
+ <div className="flex items-center justify-between py-3 border-b dark:border-gray-700">
932
+ <div>
933
+ <p className="font-medium dark:text-white">Application Name</p>
934
+ <p className="text-sm text-gray-500 dark:text-gray-400">Displayed in the browser title and header</p>
935
+ </div>
936
+ <input
937
+ type="text"
938
+ value={settings['app.name']}
939
+ onChange={(e) => setSettings({ ...settings, 'app.name': e.target.value })}
940
+ className="border dark:border-gray-600 rounded px-3 py-2 w-64 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
941
+ />
942
+ </div>
943
+
944
+ <div className="flex items-center justify-between py-3 border-b dark:border-gray-700">
945
+ <div>
946
+ <p className="font-medium dark:text-white">Default Language</p>
947
+ <p className="text-sm text-gray-500 dark:text-gray-400">Default language for prompt templates</p>
948
+ </div>
949
+ <select
950
+ value={settings['app.language']}
951
+ onChange={(e) => setSettings({ ...settings, 'app.language': e.target.value })}
952
+ className="border dark:border-gray-600 rounded px-3 py-2 w-64 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
953
+ >
954
+ <option value="en">English</option>
955
+ <option value="de">Deutsch</option>
956
+ <option value="fr">Fran\u00e7ais</option>
957
+ <option value="es">Espa\u00f1ol</option>
958
+ <option value="it">Italiano</option>
959
+ </select>
960
+ </div>
961
+
962
+ <div className="flex items-center justify-between py-3">
963
+ <div>
964
+ <p className="font-medium dark:text-white">Maintenance Mode</p>
965
+ <p className="text-sm text-gray-500 dark:text-gray-400">Restrict access to admins only</p>
966
+ </div>
967
+ <button
968
+ onClick={() =>
969
+ setSettings({
970
+ ...settings,
971
+ 'app.maintenanceMode': settings['app.maintenanceMode'] === 'true' ? 'false' : 'true',
972
+ })
973
+ }
974
+ className={\`px-4 py-2 rounded font-medium text-sm transition-colors \${
975
+ settings['app.maintenanceMode'] === 'true'
976
+ ? 'bg-red-600 text-white hover:bg-red-700'
977
+ : 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-500'
978
+ }\`}
979
+ >
980
+ {settings['app.maintenanceMode'] === 'true' ? 'On' : 'Off'}
981
+ </button>
982
+ </div>
983
+ </div>
984
+
985
+ {/* Action buttons */}
986
+ <div className="flex items-center gap-3 pt-4 border-t dark:border-gray-700">
987
+ <button
988
+ onClick={handleSave}
989
+ disabled={!hasChanges || saving}
990
+ className="px-6 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
991
+ >
992
+ {saving ? 'Saving...' : 'Save Changes'}
993
+ </button>
994
+ <button
995
+ onClick={handleReset}
996
+ disabled={!hasChanges}
997
+ className="px-6 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-lg text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
998
+ >
999
+ Reset
1000
+ </button>
1001
+ </div>
1002
+ </div>
1003
+ </div>
1004
+ );
1005
+ }
1006
+ `;
1007
+ }
1008
+ /**
1009
+ * Generates the admin activity logs page
1010
+ * Full audit log table with filters for action, user, and date range
1011
+ * @returns TypeScript/JSX content for app/admin/logs/page.tsx
1012
+ */
1013
+ function generateAdminLogsPage() {
1014
+ return `// @chimerai component=AdminLogsPage version=2.0
1015
+ 'use client';
1016
+
1017
+ import { useEffect, useState } from 'react';
1018
+
1019
+ interface AuditLogEntry {
1020
+ id: string;
1021
+ action: string;
1022
+ userId: string;
1023
+ targetType: string | null;
1024
+ targetId: string | null;
1025
+ metadata: any;
1026
+ ipAddress: string | null;
1027
+ createdAt: string;
1028
+ user: { name: string | null; email: string | null };
1029
+ }
1030
+
1031
+ export default function AdminLogsPage() {
1032
+ const [logs, setLogs] = useState<AuditLogEntry[]>([]);
1033
+ const [loading, setLoading] = useState(true);
1034
+ const [actionFilter, setActionFilter] = useState('');
1035
+ const [page, setPage] = useState(1);
1036
+ const [totalPages, setTotalPages] = useState(1);
1037
+ const pageSize = 25;
1038
+
1039
+ const fetchLogs = async () => {
1040
+ setLoading(true);
1041
+ try {
1042
+ const params = new URLSearchParams({
1043
+ page: page.toString(),
1044
+ pageSize: pageSize.toString(),
1045
+ });
1046
+ if (actionFilter) params.set('action', actionFilter);
1047
+
1048
+ const res = await fetch(\`/api/admin/audit-logs?\${params}\`);
1049
+ if (res.ok) {
1050
+ const data = await res.json();
1051
+ setLogs(data.logs);
1052
+ setTotalPages(Math.ceil(data.total / pageSize));
1053
+ }
1054
+ } catch (err) {
1055
+ console.error('Failed to fetch audit logs:', err);
1056
+ } finally {
1057
+ setLoading(false);
1058
+ }
1059
+ };
1060
+
1061
+ useEffect(() => {
1062
+ fetchLogs();
1063
+ }, [page, actionFilter]);
1064
+
1065
+ const formatDate = (dateStr: string) => {
1066
+ return new Date(dateStr).toLocaleString();
1067
+ };
1068
+
1069
+ const actionBadgeColor = (action: string) => {
1070
+ if (action.includes('create')) return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
1071
+ if (action.includes('update')) return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
1072
+ if (action.includes('delete')) return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
1073
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
1074
+ };
1075
+
1076
+ return (
1077
+ <div className="container mx-auto py-8">
1078
+ <div className="mb-8">
1079
+ <h1 className="text-4xl font-bold mb-2 dark:text-white">Activity Logs</h1>
1080
+ <p className="text-gray-600 dark:text-gray-400">Monitor system activity and user actions</p>
1081
+ </div>
1082
+
1083
+ {/* Filters */}
1084
+ <div className="mb-4 flex gap-4">
1085
+ <select
1086
+ value={actionFilter}
1087
+ onChange={(e) => { setActionFilter(e.target.value); setPage(1); }}
1088
+ className="px-3 py-2 border rounded-lg bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-white"
1089
+ >
1090
+ <option value="">All Actions</option>
1091
+ <option value="user.create">User Created</option>
1092
+ <option value="user.update">User Updated</option>
1093
+ <option value="user.delete">User Deleted</option>
1094
+ <option value="role.create">Role Created</option>
1095
+ <option value="role.update">Role Updated</option>
1096
+ <option value="role.delete">Role Deleted</option>
1097
+ <option value="provider.create">Provider Created</option>
1098
+ <option value="provider.update">Provider Updated</option>
1099
+ <option value="provider.delete">Provider Deleted</option>
1100
+ <option value="user.login">User Login</option>
1101
+ </select>
1102
+ </div>
1103
+
1104
+ {/* Table */}
1105
+ <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
1106
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
1107
+ <thead className="bg-gray-50 dark:bg-gray-900">
1108
+ <tr>
1109
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Time</th>
1110
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Action</th>
1111
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">User</th>
1112
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Target</th>
1113
+ <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Details</th>
1114
+ </tr>
1115
+ </thead>
1116
+ <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
1117
+ {loading ? (
1118
+ <tr>
1119
+ <td colSpan={5} className="px-4 py-8 text-center text-gray-400">
1120
+ Loading...
1121
+ </td>
1122
+ </tr>
1123
+ ) : logs.length === 0 ? (
1124
+ <tr>
1125
+ <td colSpan={5} className="px-4 py-8 text-center text-gray-400">
1126
+ No audit logs found
1127
+ </td>
1128
+ </tr>
1129
+ ) : (
1130
+ logs.map((log) => (
1131
+ <tr key={log.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
1132
+ <td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
1133
+ {formatDate(log.createdAt)}
1134
+ </td>
1135
+ <td className="px-4 py-3">
1136
+ <span className={\`inline-flex px-2 py-1 text-xs font-medium rounded-full \${actionBadgeColor(log.action)}\`}>
1137
+ {log.action}
1138
+ </span>
1139
+ </td>
1140
+ <td className="px-4 py-3 text-sm dark:text-gray-300">
1141
+ {log.user?.name || log.user?.email || log.userId}
1142
+ </td>
1143
+ <td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
1144
+ {log.targetType && (
1145
+ <span>{log.targetType}{log.targetId ? \`: \${log.targetId.slice(0, 8)}...\` : ''}</span>
1146
+ )}
1147
+ </td>
1148
+ <td className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
1149
+ {log.metadata && (
1150
+ <pre className="text-xs max-w-xs truncate">{JSON.stringify(log.metadata)}</pre>
1151
+ )}
1152
+ </td>
1153
+ </tr>
1154
+ ))
1155
+ )}
1156
+ </tbody>
1157
+ </table>
1158
+ </div>
1159
+
1160
+ {/* Pagination */}
1161
+ {totalPages > 1 && (
1162
+ <div className="mt-4 flex justify-center gap-2">
1163
+ <button
1164
+ onClick={() => setPage(Math.max(1, page - 1))}
1165
+ disabled={page === 1}
1166
+ className="px-3 py-1 rounded border dark:border-gray-600 disabled:opacity-50 dark:text-white"
1167
+ >
1168
+ Previous
1169
+ </button>
1170
+ <span className="px-3 py-1 text-sm dark:text-gray-300">
1171
+ Page {page} of {totalPages}
1172
+ </span>
1173
+ <button
1174
+ onClick={() => setPage(Math.min(totalPages, page + 1))}
1175
+ disabled={page === totalPages}
1176
+ className="px-3 py-1 rounded border dark:border-gray-600 disabled:opacity-50 dark:text-white"
1177
+ >
1178
+ Next
1179
+ </button>
1180
+ </div>
1181
+ )}
1182
+ </div>
1183
+ );
1184
+ }
1185
+ `;
1186
+ }
1187
+ /**
1188
+ * Generates the audit log helper utility.
1189
+ * Provides logAuditAction() for recording actions across the app.
1190
+ * @returns TypeScript content for lib/audit.ts
1191
+ */
1192
+ function generatePermissionsLib() {
1193
+ return `/**
1194
+ * Permission utility functions
1195
+ * Handles permission checks and role-based access control
1196
+ */
1197
+
1198
+ export const AVAILABLE_PERMISSIONS = [
1199
+ 'users:read',
1200
+ 'users:write',
1201
+ 'users:delete',
1202
+ 'roles:read',
1203
+ 'roles:write',
1204
+ 'roles:delete',
1205
+ 'settings:read',
1206
+ 'settings:write',
1207
+ 'admin:*',
1208
+ ] as const;
1209
+
1210
+ export type Permission = typeof AVAILABLE_PERMISSIONS[number];
1211
+
1212
+ interface User {
1213
+ id: string;
1214
+ email: string;
1215
+ roles?: Array<{ permissions: string[] }>;
1216
+ }
1217
+
1218
+ export function hasPermission(user: User | null, permission: string): boolean {
1219
+ if (!user || !user.roles) return false;
1220
+ const allPermissions = user.roles.flatMap(role => role.permissions || []);
1221
+
1222
+ // Tier 1: Super-Wildcard — '*' matcht ALLES
1223
+ if (allPermissions.includes('*')) return true;
1224
+
1225
+ // Tier 2: Kategorie-Wildcard — 'admin:*' matcht 'admin:users:read', 'admin:roles:write', etc.
1226
+ for (const perm of allPermissions) {
1227
+ if (perm.endsWith(':*')) {
1228
+ const prefix = perm.slice(0, -1); // 'admin:*' → 'admin:'
1229
+ if (permission.startsWith(prefix)) return true;
1230
+ }
1231
+ }
1232
+
1233
+ // Tier 3: Exakter Match
1234
+ return allPermissions.includes(permission);
1235
+ }
1236
+
1237
+ export function hasAnyPermission(user: User | null, permissions: string[]): boolean {
1238
+ if (!user) return false;
1239
+ return permissions.some(permission => hasPermission(user, permission));
1240
+ }
1241
+
1242
+ export function hasAllPermissions(user: User | null, permissions: string[]): boolean {
1243
+ if (!user) return false;
1244
+ return permissions.every(permission => hasPermission(user, permission));
1245
+ }
1246
+
1247
+ export function getUserPermissions(user: User | null): string[] {
1248
+ if (!user || !user.roles) return [];
1249
+ return [...new Set(user.roles.flatMap(role => role.permissions || []))];
1250
+ }
1251
+
1252
+ export function isValidPermission(permission: string): boolean {
1253
+ return AVAILABLE_PERMISSIONS.includes(permission as Permission);
1254
+ }
1255
+ `;
1256
+ }
1257
+ function generateRequirePermissionLib() {
1258
+ return `import { getServerSession } from 'next-auth';
1259
+ import { NextResponse } from 'next/server';
1260
+ import { authOptions } from '@/lib/auth';
1261
+ import { hasPermission } from '@/lib/permissions';
1262
+
1263
+ /**
1264
+ * Server-side permission check for API routes
1265
+ * Returns NextResponse with 401/403 if check fails, null if OK
1266
+ */
1267
+ export async function requirePermission(permission: string) {
1268
+ const session = await getServerSession(authOptions);
1269
+
1270
+ if (!session || !session.user) {
1271
+ return NextResponse.json(
1272
+ { error: 'Unauthorized - Please sign in' },
1273
+ { status: 401 }
1274
+ );
1275
+ }
1276
+
1277
+ const user = await getServerSessionWithPermissions();
1278
+
1279
+ if (!user || !hasPermission(user as any, permission)) {
1280
+ return NextResponse.json(
1281
+ { error: \`Forbidden - Required permission: \${permission}\` },
1282
+ { status: 403 }
1283
+ );
1284
+ }
1285
+
1286
+ return null;
1287
+ }
1288
+
1289
+ async function getServerSessionWithPermissions() {
1290
+ const session = await getServerSession(authOptions);
1291
+ if (!session?.user?.email) return null;
1292
+
1293
+ const { prisma } = await import('@/lib/prisma');
1294
+ const user = await prisma.user.findUnique({
1295
+ where: { email: session.user.email },
1296
+ include: {
1297
+ roles: {
1298
+ select: {
1299
+ role: {
1300
+ select: {
1301
+ id: true,
1302
+ name: true,
1303
+ permissions: true
1304
+ }
1305
+ }
1306
+ }
1307
+ }
1308
+ }
1309
+ });
1310
+
1311
+ if (!user) return null;
1312
+
1313
+ // Flatten UserRole[] → { permissions: string[] }[]
1314
+ return {
1315
+ ...user,
1316
+ roles: (user.roles as any[]).map((ur: any) => ur.role),
1317
+ };
1318
+ }
1319
+ `;
1320
+ }
1321
+ function generateAuditLogHelper() {
1322
+ return `// @chimerai component=AuditLogHelper version=1.0
1323
+ import { prisma } from './prisma';
1324
+
1325
+ export interface AuditActionParams {
1326
+ action: string;
1327
+ userId: string;
1328
+ targetType?: string;
1329
+ targetId?: string;
1330
+ metadata?: Record<string, any>;
1331
+ ipAddress?: string;
1332
+ }
1333
+
1334
+ /**
1335
+ * Log an audit action. Fire-and-forget — errors are caught and logged.
1336
+ */
1337
+ export async function logAuditAction(params: AuditActionParams): Promise<void> {
1338
+ try {
1339
+ if (!(prisma as any).auditLog) return; // AuditLog model not in schema (requires admin-ui)
1340
+ await (prisma as any).auditLog.create({
1341
+ data: {
1342
+ action: params.action,
1343
+ userId: params.userId,
1344
+ targetType: params.targetType || null,
1345
+ targetId: params.targetId || null,
1346
+ metadata: params.metadata ? JSON.stringify(params.metadata) : null,
1347
+ ipAddress: params.ipAddress || null,
1348
+ },
1349
+ });
1350
+ } catch (error) {
1351
+ console.error('[AuditLog] Failed to log action:', params.action, error);
1352
+ }
1353
+ }
1354
+
1355
+ /** Alias for backwards compatibility and short-form usage */
1356
+ export const logAction = logAuditAction;
1357
+ `;
1358
+ }