nobalmako 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +112 -0
  2. package/components.json +22 -0
  3. package/dist/nobalmako.js +272 -0
  4. package/drizzle/0000_pink_spiral.sql +126 -0
  5. package/drizzle/meta/0000_snapshot.json +1027 -0
  6. package/drizzle/meta/_journal.json +13 -0
  7. package/drizzle.config.ts +10 -0
  8. package/eslint.config.mjs +18 -0
  9. package/next.config.ts +7 -0
  10. package/package.json +80 -0
  11. package/postcss.config.mjs +7 -0
  12. package/public/file.svg +1 -0
  13. package/public/globe.svg +1 -0
  14. package/public/next.svg +1 -0
  15. package/public/vercel.svg +1 -0
  16. package/public/window.svg +1 -0
  17. package/server/index.ts +118 -0
  18. package/src/app/api/api-keys/[id]/route.ts +147 -0
  19. package/src/app/api/api-keys/route.ts +151 -0
  20. package/src/app/api/audit-logs/route.ts +84 -0
  21. package/src/app/api/auth/forgot-password/route.ts +47 -0
  22. package/src/app/api/auth/login/route.ts +99 -0
  23. package/src/app/api/auth/logout/route.ts +15 -0
  24. package/src/app/api/auth/me/route.ts +23 -0
  25. package/src/app/api/auth/mfa/setup/route.ts +33 -0
  26. package/src/app/api/auth/mfa/verify/route.ts +45 -0
  27. package/src/app/api/auth/register/route.ts +140 -0
  28. package/src/app/api/auth/reset-password/route.ts +52 -0
  29. package/src/app/api/auth/update/route.ts +71 -0
  30. package/src/app/api/auth/verify/route.ts +39 -0
  31. package/src/app/api/environments/route.ts +227 -0
  32. package/src/app/api/team-members/route.ts +385 -0
  33. package/src/app/api/teams/route.ts +217 -0
  34. package/src/app/api/variable-history/route.ts +218 -0
  35. package/src/app/api/variables/route.ts +476 -0
  36. package/src/app/api/webhooks/route.ts +77 -0
  37. package/src/app/api-keys/APIKeysClient.tsx +316 -0
  38. package/src/app/api-keys/page.tsx +10 -0
  39. package/src/app/api-reference/page.tsx +324 -0
  40. package/src/app/audit-log/AuditLogClient.tsx +229 -0
  41. package/src/app/audit-log/page.tsx +10 -0
  42. package/src/app/auth/forgot-password/page.tsx +121 -0
  43. package/src/app/auth/login/LoginForm.tsx +145 -0
  44. package/src/app/auth/login/page.tsx +11 -0
  45. package/src/app/auth/register/RegisterForm.tsx +156 -0
  46. package/src/app/auth/register/page.tsx +16 -0
  47. package/src/app/auth/reset-password/page.tsx +160 -0
  48. package/src/app/dashboard/DashboardClient.tsx +219 -0
  49. package/src/app/dashboard/page.tsx +11 -0
  50. package/src/app/docs/page.tsx +251 -0
  51. package/src/app/favicon.ico +0 -0
  52. package/src/app/globals.css +123 -0
  53. package/src/app/layout.tsx +35 -0
  54. package/src/app/page.tsx +231 -0
  55. package/src/app/profile/ProfileClient.tsx +230 -0
  56. package/src/app/profile/page.tsx +10 -0
  57. package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
  58. package/src/app/project/[id]/page.tsx +17 -0
  59. package/src/bin/nobalmako.ts +341 -0
  60. package/src/components/ApiKeysManager.tsx +529 -0
  61. package/src/components/AppLayout.tsx +193 -0
  62. package/src/components/BulkActions.tsx +138 -0
  63. package/src/components/CreateEnvironmentDialog.tsx +207 -0
  64. package/src/components/CreateTeamDialog.tsx +174 -0
  65. package/src/components/CreateVariableDialog.tsx +311 -0
  66. package/src/components/DeleteEnvironmentDialog.tsx +104 -0
  67. package/src/components/DeleteTeamDialog.tsx +112 -0
  68. package/src/components/DeleteVariableDialog.tsx +103 -0
  69. package/src/components/EditEnvironmentDialog.tsx +202 -0
  70. package/src/components/EditMemberDialog.tsx +143 -0
  71. package/src/components/EditTeamDialog.tsx +178 -0
  72. package/src/components/EditVariableDialog.tsx +231 -0
  73. package/src/components/ImportVariablesDialog.tsx +347 -0
  74. package/src/components/InviteMemberDialog.tsx +191 -0
  75. package/src/components/LeaveProjectDialog.tsx +111 -0
  76. package/src/components/MFASettings.tsx +136 -0
  77. package/src/components/ProjectDiff.tsx +123 -0
  78. package/src/components/Providers.tsx +24 -0
  79. package/src/components/RemoveMemberDialog.tsx +112 -0
  80. package/src/components/SearchDialog.tsx +276 -0
  81. package/src/components/SecurityOverview.tsx +92 -0
  82. package/src/components/TeamMembersManager.tsx +103 -0
  83. package/src/components/VariableHistoryDialog.tsx +265 -0
  84. package/src/components/WebhooksManager.tsx +169 -0
  85. package/src/components/ui/alert-dialog.tsx +160 -0
  86. package/src/components/ui/alert.tsx +59 -0
  87. package/src/components/ui/avatar.tsx +53 -0
  88. package/src/components/ui/badge.tsx +46 -0
  89. package/src/components/ui/button.tsx +62 -0
  90. package/src/components/ui/card.tsx +92 -0
  91. package/src/components/ui/checkbox.tsx +32 -0
  92. package/src/components/ui/dialog.tsx +143 -0
  93. package/src/components/ui/dropdown-menu.tsx +257 -0
  94. package/src/components/ui/input.tsx +21 -0
  95. package/src/components/ui/label.tsx +24 -0
  96. package/src/components/ui/select.tsx +190 -0
  97. package/src/components/ui/separator.tsx +28 -0
  98. package/src/components/ui/sonner.tsx +37 -0
  99. package/src/components/ui/switch.tsx +31 -0
  100. package/src/components/ui/table.tsx +117 -0
  101. package/src/components/ui/tabs.tsx +66 -0
  102. package/src/components/ui/textarea.tsx +18 -0
  103. package/src/hooks/use-api-keys.ts +95 -0
  104. package/src/hooks/use-audit-logs.ts +58 -0
  105. package/src/hooks/use-auth.tsx +121 -0
  106. package/src/hooks/use-environments.ts +33 -0
  107. package/src/hooks/use-project-permissions.ts +49 -0
  108. package/src/hooks/use-team-members.ts +30 -0
  109. package/src/hooks/use-teams.ts +33 -0
  110. package/src/hooks/use-variables.ts +38 -0
  111. package/src/lib/audit.ts +36 -0
  112. package/src/lib/auth.ts +108 -0
  113. package/src/lib/crypto.ts +39 -0
  114. package/src/lib/db.ts +15 -0
  115. package/src/lib/dynamic-providers.ts +19 -0
  116. package/src/lib/email.ts +110 -0
  117. package/src/lib/mail.ts +51 -0
  118. package/src/lib/permissions.ts +51 -0
  119. package/src/lib/schema.ts +240 -0
  120. package/src/lib/seed.ts +107 -0
  121. package/src/lib/utils.ts +6 -0
  122. package/src/lib/webhooks.ts +42 -0
  123. package/tsconfig.json +34 -0
@@ -0,0 +1,476 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getUserFromToken } from '@/lib/auth';
3
+ import { db } from '@/lib/db';
4
+ import { environmentVariables, teams, environments, variableHistory, teamMembers } from '@/lib/schema';
5
+ import { eq, and, or, exists } from 'drizzle-orm';
6
+ import { encrypt, decrypt } from '@/lib/crypto';
7
+ import { createAuditLog } from '@/lib/audit';
8
+ import { hasPermission, Permissions } from '@/lib/permissions';
9
+ import { triggerWebhook } from '@/lib/webhooks';
10
+ import { resolveDynamicSecret } from '@/lib/dynamic-providers';
11
+
12
+ export async function GET(request: NextRequest) {
13
+ try {
14
+ const user = await getUserFromToken();
15
+
16
+ if (!user) {
17
+ return NextResponse.json(
18
+ { error: 'Not authenticated' },
19
+ { status: 401 }
20
+ );
21
+ }
22
+
23
+ const { searchParams } = new URL(request.url);
24
+ const teamName = searchParams.get('team');
25
+ const environmentName = searchParams.get('environment');
26
+ const teamId = searchParams.get('teamId');
27
+ const environmentId = searchParams.get('environmentId');
28
+
29
+ // Build the query
30
+ let query = db
31
+ .select({
32
+ id: environmentVariables.id,
33
+ key: environmentVariables.key,
34
+ value: environmentVariables.value,
35
+ description: environmentVariables.description,
36
+ isSecret: environmentVariables.isSecret,
37
+ teamId: environmentVariables.teamId,
38
+ environmentId: environmentVariables.environmentId,
39
+ isDynamic: environmentVariables.isDynamic,
40
+ provider: environmentVariables.provider,
41
+ createdAt: environmentVariables.createdAt,
42
+ teamName: teams.name,
43
+ environmentName: environments.name,
44
+ })
45
+ .from(environmentVariables)
46
+ .innerJoin(environments, eq(environmentVariables.environmentId, environments.id))
47
+ .innerJoin(teams, eq(environmentVariables.teamId, teams.id));
48
+
49
+ // Base filters: User must have access
50
+ const accessFilter = or(
51
+ eq(teams.ownerId, user.id),
52
+ exists(
53
+ db.select()
54
+ .from(teamMembers)
55
+ .where(and(eq(teamMembers.teamId, teams.id), eq(teamMembers.userId, user.id)))
56
+ )
57
+ );
58
+
59
+ const filters = [accessFilter];
60
+
61
+ if (teamName) filters.push(eq(teams.name, teamName));
62
+ if (environmentName) filters.push(eq(environments.name, environmentName));
63
+ if (teamId) filters.push(eq(teams.id, teamId));
64
+
65
+ // Inheritance Logic
66
+ let environmentIds: string[] = [];
67
+ if (environmentId || (teamName && environmentName) || (teamId && environmentName)) {
68
+ // Find the specific environment first
69
+ let targetEnv;
70
+ if (environmentId) {
71
+ [targetEnv] = await db.select().from(environments).where(eq(environments.id, environmentId)).limit(1);
72
+ } else {
73
+ const teamFilter = teamId ? eq(teams.id, teamId) : eq(teams.name, teamName!);
74
+ [targetEnv] = await db.select({
75
+ id: environments.id,
76
+ parentId: environments.parentId
77
+ })
78
+ .from(environments)
79
+ .innerJoin(teams, eq(environments.teamId, teams.id))
80
+ .where(and(teamFilter, eq(environments.name, environmentName!)))
81
+ .limit(1);
82
+ }
83
+
84
+ if (targetEnv) {
85
+ environmentIds.push(targetEnv.id);
86
+ let currentParentId = targetEnv.parentId;
87
+
88
+ // Traverse up the inheritance tree (limit depth to 10 to prevent cycles)
89
+ for (let i = 0; i < 10 && currentParentId; i++) {
90
+ const [parent] = await db.select().from(environments).where(eq(environments.id, currentParentId)).limit(1);
91
+ if (parent) {
92
+ environmentIds.push(parent.id);
93
+ currentParentId = parent.parentId;
94
+ } else {
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ if (environmentIds.length > 0) {
102
+ // @ts-ignore
103
+ filters.push(or(...environmentIds.map(id => eq(environments.id, id))));
104
+ } else if (environmentName) {
105
+ filters.push(eq(environments.name, environmentName));
106
+ }
107
+
108
+ const userVariables = await query.where(and(...filters));
109
+
110
+ // Decrypt and Merge duplicates (Child variables overwrite parents)
111
+ // environmentIds is [child, parent, grandparent...]
112
+ // So we iterate backwards to let child values win.
113
+ const mergedVars: Record<string, any> = {};
114
+
115
+ // Sort variables by their index in environmentIds (higher index means further up the tree)
116
+ const sortedVariables = [...userVariables].sort((a, b) => {
117
+ const indexA = environmentIds.indexOf(a.environmentId);
118
+ const indexB = environmentIds.indexOf(b.environmentId);
119
+ return (indexB === -1 ? 999 : indexB) - (indexA === -1 ? 999 : indexA);
120
+ });
121
+
122
+ for (const v of sortedVariables) {
123
+ let value = decrypt(v.value);
124
+
125
+ if (v.isDynamic) {
126
+ value = await resolveDynamicSecret(v);
127
+ }
128
+
129
+ mergedVars[v.key] = {
130
+ ...v,
131
+ value
132
+ };
133
+ }
134
+
135
+ return NextResponse.json({ variables: Object.values(mergedVars) });
136
+ } catch (error) {
137
+ console.error('Get variables error:', error);
138
+ return NextResponse.json(
139
+ { error: 'Internal server error' },
140
+ { status: 500 }
141
+ );
142
+ }
143
+ }
144
+
145
+ export async function POST(request: NextRequest) {
146
+ try {
147
+ const user = await getUserFromToken();
148
+
149
+ if (!user) {
150
+ return NextResponse.json(
151
+ { error: 'Not authenticated' },
152
+ { status: 401 }
153
+ );
154
+ }
155
+
156
+ const { key, value, description, isSecret, teamId, environmentId, expiresAt, isDynamic, provider } = await request.json();
157
+
158
+ if (!key || (value === undefined && !isDynamic) || !teamId || !environmentId) {
159
+ return NextResponse.json(
160
+ { error: 'Key, value (or dynamic provider), teamId, and environmentId are required' },
161
+ { status: 400 }
162
+ );
163
+ }
164
+
165
+ const encryptedValue = encrypt(value || '');
166
+
167
+ // Verify user has permission to manage secrets
168
+ const canManage = await hasPermission(user.id, teamId, Permissions.MANAGE_SECRETS);
169
+
170
+ if (!canManage) {
171
+ return NextResponse.json(
172
+ { error: 'Forbidden: You do not have permission to manage secrets in this project' },
173
+ { status: 403 }
174
+ );
175
+ }
176
+
177
+ // Verify environment belongs to the team
178
+ const [environment] = await db
179
+ .select()
180
+ .from(environments)
181
+ .where(and(eq(environments.id, environmentId), eq(environments.teamId, teamId)))
182
+ .limit(1);
183
+
184
+ if (!environment) {
185
+ return NextResponse.json(
186
+ { error: 'Environment not found or does not belong to the team' },
187
+ { status: 403 }
188
+ );
189
+ }
190
+
191
+ // Check if variable already exists in this environment
192
+ const [existingVariable] = await db
193
+ .select()
194
+ .from(environmentVariables)
195
+ .where(
196
+ and(
197
+ eq(environmentVariables.key, key),
198
+ eq(environmentVariables.environmentId, environmentId)
199
+ )
200
+ )
201
+ .limit(1);
202
+
203
+ let variable;
204
+ let changeType: 'create' | 'update' = 'create';
205
+
206
+ if (existingVariable) {
207
+ // Update existing variable
208
+ changeType = 'update';
209
+ const [updatedVariable] = await db
210
+ .update(environmentVariables)
211
+ .set({
212
+ value: encryptedValue,
213
+ description: description || existingVariable.description,
214
+ isSecret: isSecret !== undefined ? isSecret : existingVariable.isSecret,
215
+ isDynamic: isDynamic !== undefined ? isDynamic : existingVariable.isDynamic,
216
+ provider: provider || existingVariable.provider,
217
+ expiresAt: expiresAt !== undefined ? expiresAt : existingVariable.expiresAt,
218
+ lastRotatedAt: value !== decrypt(existingVariable.value) ? new Date() : existingVariable.lastRotatedAt,
219
+ updatedBy: user.id,
220
+ updatedAt: new Date(),
221
+ })
222
+ .where(eq(environmentVariables.id, existingVariable.id))
223
+ .returning();
224
+ variable = updatedVariable;
225
+ } else {
226
+ // Insert new variable
227
+ const [newVariable] = await db.insert(environmentVariables).values({
228
+ key,
229
+ value: encryptedValue,
230
+ description,
231
+ isSecret: isSecret || false,
232
+ isDynamic: isDynamic || false,
233
+ provider: provider || null,
234
+ expiresAt: expiresAt || null,
235
+ lastRotatedAt: new Date(),
236
+ teamId,
237
+ environmentId,
238
+ createdBy: user.id,
239
+ }).returning();
240
+ variable = newVariable;
241
+ }
242
+
243
+ // Create history entry
244
+ await db.insert(variableHistory).values({
245
+ variableId: variable.id,
246
+ teamId,
247
+ environmentId,
248
+ key,
249
+ value: encryptedValue,
250
+ description: description || variable.description,
251
+ isSecret: variable.isSecret,
252
+ changedBy: user.id,
253
+ changeType,
254
+ });
255
+
256
+ // Trigger Webhook
257
+ triggerWebhook(teamId, 'variable.update', {
258
+ key,
259
+ environmentId,
260
+ changeType,
261
+ updatedBy: user.email,
262
+ });
263
+
264
+ // Create audit log
265
+ await createAuditLog({
266
+ userId: user.id,
267
+ teamId,
268
+ action: 'create',
269
+ resourceType: 'variable',
270
+ resourceId: variable.id,
271
+ newValue: { key, isSecret },
272
+ });
273
+
274
+ return NextResponse.json({ variable: variable });
275
+ } catch (error) {
276
+ console.error('Create variable error:', error);
277
+ return NextResponse.json(
278
+ { error: 'Internal server error' },
279
+ { status: 500 }
280
+ );
281
+ }
282
+ }
283
+ export async function PUT(request: NextRequest) {
284
+ try {
285
+ const user = await getUserFromToken();
286
+
287
+ if (!user) {
288
+ return NextResponse.json(
289
+ { error: 'Not authenticated' },
290
+ { status: 401 }
291
+ );
292
+ }
293
+
294
+ const { id, key, value, description, isSecret, expiresAt, isDynamic, provider } = await request.json();
295
+
296
+ if (!id) {
297
+ return NextResponse.json(
298
+ { error: 'Variable ID is required' },
299
+ { status: 400 }
300
+ );
301
+ }
302
+
303
+ if (!key || (value === undefined && !isDynamic)) {
304
+ return NextResponse.json(
305
+ { error: 'Key and value (or dynamic provider) are required' },
306
+ { status: 400 }
307
+ );
308
+ }
309
+
310
+ // Check if user has permission to manage secrets
311
+ const existingVar = await db
312
+ .select()
313
+ .from(environmentVariables)
314
+ .where(eq(environmentVariables.id, id))
315
+ .limit(1);
316
+
317
+ if (!existingVar.length) {
318
+ return NextResponse.json(
319
+ { error: 'Variable not found' },
320
+ { status: 404 }
321
+ );
322
+ }
323
+
324
+ const canManage = await hasPermission(user.id, existingVar[0].teamId, Permissions.MANAGE_SECRETS);
325
+ if (!canManage) {
326
+ return NextResponse.json(
327
+ { error: 'Forbidden: You do not have permission to manage secrets in this project' },
328
+ { status: 403 }
329
+ );
330
+ }
331
+
332
+ // Encrypt the value
333
+ const encryptedValue = encrypt(value || '');
334
+
335
+ const [updatedVariable] = await db
336
+ .update(environmentVariables)
337
+ .set({
338
+ key,
339
+ value: encryptedValue,
340
+ description,
341
+ isSecret: isSecret !== undefined ? isSecret : existingVar[0].isSecret,
342
+ isDynamic: isDynamic !== undefined ? isDynamic : existingVar[0].isDynamic,
343
+ provider: provider || existingVar[0].provider,
344
+ expiresAt: expiresAt !== undefined ? expiresAt : existingVar[0].expiresAt,
345
+ lastRotatedAt: value !== decrypt(existingVar[0].value) ? new Date() : existingVar[0].lastRotatedAt,
346
+ updatedAt: new Date(),
347
+ updatedBy: user.id,
348
+ })
349
+ .where(eq(environmentVariables.id, id))
350
+ .returning();
351
+
352
+ // Create history entry
353
+ await db.insert(variableHistory).values({
354
+ variableId: id,
355
+ teamId: existingVar[0].teamId,
356
+ environmentId: existingVar[0].environmentId,
357
+ key,
358
+ value: encryptedValue,
359
+ description,
360
+ isSecret: isSecret !== undefined ? isSecret : existingVar[0].isSecret,
361
+ changedBy: user.id,
362
+ changeType: 'update',
363
+ });
364
+
365
+ // Trigger Webhook
366
+ triggerWebhook(existingVar[0].teamId, 'variable.update', {
367
+ key,
368
+ environmentId: existingVar[0].environmentId,
369
+ changeType: 'update',
370
+ updatedBy: user.email,
371
+ });
372
+
373
+ // Create audit log
374
+ await createAuditLog({
375
+ userId: user.id,
376
+ teamId: existingVar[0].teamId,
377
+ action: 'update',
378
+ resourceType: 'variable',
379
+ resourceId: id,
380
+ oldValue: { key: existingVar[0].key, isSecret: existingVar[0].isSecret },
381
+ newValue: { key, isSecret },
382
+ });
383
+
384
+ return NextResponse.json({ variable: updatedVariable });
385
+ } catch (error) {
386
+ console.error('Update variable error:', error);
387
+ return NextResponse.json(
388
+ { error: 'Internal server error' },
389
+ { status: 500 }
390
+ );
391
+ }
392
+ }
393
+
394
+ export async function DELETE(request: NextRequest) {
395
+ try {
396
+ const user = await getUserFromToken();
397
+
398
+ if (!user) {
399
+ return NextResponse.json(
400
+ { error: 'Not authenticated' },
401
+ { status: 401 }
402
+ );
403
+ }
404
+
405
+ const { id } = await request.json();
406
+
407
+ if (!id) {
408
+ return NextResponse.json(
409
+ { error: 'Variable ID is required' },
410
+ { status: 400 }
411
+ );
412
+ }
413
+
414
+ // Check if user has permission to manage secrets
415
+ const existingVar = await db
416
+ .select()
417
+ .from(environmentVariables)
418
+ .where(eq(environmentVariables.id, id))
419
+ .limit(1);
420
+
421
+ if (!existingVar.length) {
422
+ return NextResponse.json(
423
+ { error: 'Variable not found' },
424
+ { status: 404 }
425
+ );
426
+ }
427
+
428
+ const canManage = await hasPermission(user.id, existingVar[0].teamId, Permissions.MANAGE_SECRETS);
429
+ if (!canManage) {
430
+ return NextResponse.json(
431
+ { error: 'Forbidden: You do not have permission to manage secrets in this project' },
432
+ { status: 403 }
433
+ );
434
+ }
435
+
436
+ // Create history entry for deletion
437
+ await db.insert(variableHistory).values({
438
+ variableId: id,
439
+ teamId: existingVar[0].teamId,
440
+ environmentId: existingVar[0].environmentId,
441
+ key: existingVar[0].key,
442
+ value: existingVar[0].value,
443
+ description: existingVar[0].description,
444
+ isSecret: existingVar[0].isSecret,
445
+ changedBy: user.id,
446
+ changeType: 'delete',
447
+ });
448
+
449
+ await db.delete(environmentVariables).where(eq(environmentVariables.id, id));
450
+
451
+ // Trigger Webhook
452
+ triggerWebhook(existingVar[0].teamId, 'variable.delete', {
453
+ key: existingVar[0].key,
454
+ environmentId: existingVar[0].environmentId,
455
+ deletedBy: user.email,
456
+ });
457
+
458
+ // Create audit log
459
+ await createAuditLog({
460
+ userId: user.id,
461
+ teamId: existingVar[0].teamId,
462
+ action: 'delete',
463
+ resourceType: 'variable',
464
+ resourceId: id,
465
+ oldValue: { key: existingVar[0].key },
466
+ });
467
+
468
+ return NextResponse.json({ success: true });
469
+ } catch (error) {
470
+ console.error('Delete variable error:', error);
471
+ return NextResponse.json(
472
+ { error: 'Internal server error' },
473
+ { status: 500 }
474
+ );
475
+ }
476
+ }
@@ -0,0 +1,77 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { db } from '@/lib/db';
3
+ import { webhooks } from '@/lib/schema';
4
+ import { eq, and } from 'drizzle-orm';
5
+ import { getUserFromToken } from '@/lib/auth';
6
+ import { hasPermission, Permissions } from '@/lib/permissions';
7
+
8
+ export async function GET(request: NextRequest) {
9
+ try {
10
+ const user = await getUserFromToken();
11
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12
+
13
+ const { searchParams } = new URL(request.url);
14
+ const teamId = searchParams.get('teamId');
15
+
16
+ if (!teamId) return NextResponse.json({ error: 'teamId is required' }, { status: 400 });
17
+
18
+ const results = await db
19
+ .select()
20
+ .from(webhooks)
21
+ .where(eq(webhooks.teamId, teamId));
22
+
23
+ return NextResponse.json({ webhooks: results });
24
+ } catch (error) {
25
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
26
+ }
27
+ }
28
+
29
+ export async function POST(request: NextRequest) {
30
+ try {
31
+ const user = await getUserFromToken();
32
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
33
+
34
+ const { teamId, name, url, events, secret } = await request.json();
35
+
36
+ if (!teamId || !name || !url) {
37
+ return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
38
+ }
39
+
40
+ const canManage = await hasPermission(user.id, teamId, Permissions.MANAGE_TEAM);
41
+ if (!canManage) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
42
+
43
+ const [newWebhook] = await db.insert(webhooks).values({
44
+ teamId,
45
+ name,
46
+ url,
47
+ events: events || ['variable.update', 'variable.delete'],
48
+ secret,
49
+ }).returning();
50
+
51
+ return NextResponse.json({ webhook: newWebhook });
52
+ } catch (error) {
53
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
54
+ }
55
+ }
56
+
57
+ export async function DELETE(request: NextRequest) {
58
+ try {
59
+ const user = await getUserFromToken();
60
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
61
+
62
+ const { id } = await request.json();
63
+ if (!id) return NextResponse.json({ error: 'ID is required' }, { status: 400 });
64
+
65
+ const [webhook] = await db.select().from(webhooks).where(eq(webhooks.id, id)).limit(1);
66
+ if (!webhook) return NextResponse.json({ error: 'Not found' }, { status: 404 });
67
+
68
+ const canManage = await hasPermission(user.id, webhook.teamId, Permissions.MANAGE_TEAM);
69
+ if (!canManage) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
70
+
71
+ await db.delete(webhooks).where(eq(webhooks.id, id));
72
+
73
+ return NextResponse.json({ success: true });
74
+ } catch (error) {
75
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
76
+ }
77
+ }