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.
- package/README.md +112 -0
- package/components.json +22 -0
- package/dist/nobalmako.js +272 -0
- package/drizzle/0000_pink_spiral.sql +126 -0
- package/drizzle/meta/0000_snapshot.json +1027 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +7 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/index.ts +118 -0
- package/src/app/api/api-keys/[id]/route.ts +147 -0
- package/src/app/api/api-keys/route.ts +151 -0
- package/src/app/api/audit-logs/route.ts +84 -0
- package/src/app/api/auth/forgot-password/route.ts +47 -0
- package/src/app/api/auth/login/route.ts +99 -0
- package/src/app/api/auth/logout/route.ts +15 -0
- package/src/app/api/auth/me/route.ts +23 -0
- package/src/app/api/auth/mfa/setup/route.ts +33 -0
- package/src/app/api/auth/mfa/verify/route.ts +45 -0
- package/src/app/api/auth/register/route.ts +140 -0
- package/src/app/api/auth/reset-password/route.ts +52 -0
- package/src/app/api/auth/update/route.ts +71 -0
- package/src/app/api/auth/verify/route.ts +39 -0
- package/src/app/api/environments/route.ts +227 -0
- package/src/app/api/team-members/route.ts +385 -0
- package/src/app/api/teams/route.ts +217 -0
- package/src/app/api/variable-history/route.ts +218 -0
- package/src/app/api/variables/route.ts +476 -0
- package/src/app/api/webhooks/route.ts +77 -0
- package/src/app/api-keys/APIKeysClient.tsx +316 -0
- package/src/app/api-keys/page.tsx +10 -0
- package/src/app/api-reference/page.tsx +324 -0
- package/src/app/audit-log/AuditLogClient.tsx +229 -0
- package/src/app/audit-log/page.tsx +10 -0
- package/src/app/auth/forgot-password/page.tsx +121 -0
- package/src/app/auth/login/LoginForm.tsx +145 -0
- package/src/app/auth/login/page.tsx +11 -0
- package/src/app/auth/register/RegisterForm.tsx +156 -0
- package/src/app/auth/register/page.tsx +16 -0
- package/src/app/auth/reset-password/page.tsx +160 -0
- package/src/app/dashboard/DashboardClient.tsx +219 -0
- package/src/app/dashboard/page.tsx +11 -0
- package/src/app/docs/page.tsx +251 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +231 -0
- package/src/app/profile/ProfileClient.tsx +230 -0
- package/src/app/profile/page.tsx +10 -0
- package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
- package/src/app/project/[id]/page.tsx +17 -0
- package/src/bin/nobalmako.ts +341 -0
- package/src/components/ApiKeysManager.tsx +529 -0
- package/src/components/AppLayout.tsx +193 -0
- package/src/components/BulkActions.tsx +138 -0
- package/src/components/CreateEnvironmentDialog.tsx +207 -0
- package/src/components/CreateTeamDialog.tsx +174 -0
- package/src/components/CreateVariableDialog.tsx +311 -0
- package/src/components/DeleteEnvironmentDialog.tsx +104 -0
- package/src/components/DeleteTeamDialog.tsx +112 -0
- package/src/components/DeleteVariableDialog.tsx +103 -0
- package/src/components/EditEnvironmentDialog.tsx +202 -0
- package/src/components/EditMemberDialog.tsx +143 -0
- package/src/components/EditTeamDialog.tsx +178 -0
- package/src/components/EditVariableDialog.tsx +231 -0
- package/src/components/ImportVariablesDialog.tsx +347 -0
- package/src/components/InviteMemberDialog.tsx +191 -0
- package/src/components/LeaveProjectDialog.tsx +111 -0
- package/src/components/MFASettings.tsx +136 -0
- package/src/components/ProjectDiff.tsx +123 -0
- package/src/components/Providers.tsx +24 -0
- package/src/components/RemoveMemberDialog.tsx +112 -0
- package/src/components/SearchDialog.tsx +276 -0
- package/src/components/SecurityOverview.tsx +92 -0
- package/src/components/TeamMembersManager.tsx +103 -0
- package/src/components/VariableHistoryDialog.tsx +265 -0
- package/src/components/WebhooksManager.tsx +169 -0
- package/src/components/ui/alert-dialog.tsx +160 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/hooks/use-api-keys.ts +95 -0
- package/src/hooks/use-audit-logs.ts +58 -0
- package/src/hooks/use-auth.tsx +121 -0
- package/src/hooks/use-environments.ts +33 -0
- package/src/hooks/use-project-permissions.ts +49 -0
- package/src/hooks/use-team-members.ts +30 -0
- package/src/hooks/use-teams.ts +33 -0
- package/src/hooks/use-variables.ts +38 -0
- package/src/lib/audit.ts +36 -0
- package/src/lib/auth.ts +108 -0
- package/src/lib/crypto.ts +39 -0
- package/src/lib/db.ts +15 -0
- package/src/lib/dynamic-providers.ts +19 -0
- package/src/lib/email.ts +110 -0
- package/src/lib/mail.ts +51 -0
- package/src/lib/permissions.ts +51 -0
- package/src/lib/schema.ts +240 -0
- package/src/lib/seed.ts +107 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/webhooks.ts +42 -0
- 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
|
+
}
|