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,217 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getUserFromToken } from '@/lib/auth';
|
|
3
|
+
import { db } from '@/lib/db';
|
|
4
|
+
import { teams, teamMembers } from '@/lib/schema';
|
|
5
|
+
import { createAuditLog } from '@/lib/audit';
|
|
6
|
+
import { eq, or, exists, and } from 'drizzle-orm';
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
try {
|
|
10
|
+
const user = await getUserFromToken();
|
|
11
|
+
|
|
12
|
+
if (!user) {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: 'Not authenticated' },
|
|
15
|
+
{ status: 401 }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Get teams where user is owner or member
|
|
20
|
+
const userTeams = await db
|
|
21
|
+
.select()
|
|
22
|
+
.from(teams)
|
|
23
|
+
.where(
|
|
24
|
+
or(
|
|
25
|
+
eq(teams.ownerId, user.id),
|
|
26
|
+
exists(
|
|
27
|
+
db.select()
|
|
28
|
+
.from(teamMembers)
|
|
29
|
+
.where(and(eq(teamMembers.teamId, teams.id), eq(teamMembers.userId, user.id)))
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return NextResponse.json({ teams: userTeams });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Get teams error:', error);
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: 'Internal server error' },
|
|
39
|
+
{ status: 500 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function POST(request: NextRequest) {
|
|
45
|
+
try {
|
|
46
|
+
const user = await getUserFromToken();
|
|
47
|
+
|
|
48
|
+
if (!user) {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: 'Not authenticated' },
|
|
51
|
+
{ status: 401 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { name, description, color } = await request.json();
|
|
56
|
+
|
|
57
|
+
if (!name) {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: 'Name is required' },
|
|
60
|
+
{ status: 400 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const [newTeam] = await db.insert(teams).values({
|
|
65
|
+
name,
|
|
66
|
+
description,
|
|
67
|
+
color: color || '#3b82f6',
|
|
68
|
+
ownerId: user.id,
|
|
69
|
+
}).returning();
|
|
70
|
+
|
|
71
|
+
// Audit log
|
|
72
|
+
await createAuditLog({
|
|
73
|
+
userId: user.id,
|
|
74
|
+
teamId: newTeam.id,
|
|
75
|
+
action: 'create',
|
|
76
|
+
resourceType: 'team',
|
|
77
|
+
resourceId: newTeam.id,
|
|
78
|
+
newValue: { name, description, color: color || '#3b82f6' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return NextResponse.json({ team: newTeam });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Create team error:', error);
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{ error: 'Internal server error' },
|
|
86
|
+
{ status: 500 }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export async function PUT(request: NextRequest) {
|
|
91
|
+
try {
|
|
92
|
+
const user = await getUserFromToken();
|
|
93
|
+
|
|
94
|
+
if (!user) {
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: 'Not authenticated' },
|
|
97
|
+
{ status: 401 }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { id, name, description, color } = await request.json();
|
|
102
|
+
|
|
103
|
+
if (!id) {
|
|
104
|
+
return NextResponse.json(
|
|
105
|
+
{ error: 'Team ID is required' },
|
|
106
|
+
{ status: 400 }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!name) {
|
|
111
|
+
return NextResponse.json(
|
|
112
|
+
{ error: 'Name is required' },
|
|
113
|
+
{ status: 400 }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if user owns the team
|
|
118
|
+
const existingTeam = await db
|
|
119
|
+
.select()
|
|
120
|
+
.from(teams)
|
|
121
|
+
.where(eq(teams.id, id))
|
|
122
|
+
.limit(1);
|
|
123
|
+
|
|
124
|
+
if (!existingTeam.length || existingTeam[0].ownerId !== user.id) {
|
|
125
|
+
return NextResponse.json(
|
|
126
|
+
{ error: 'Team not found or access denied' },
|
|
127
|
+
{ status: 404 }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const [updatedTeam] = await db
|
|
132
|
+
.update(teams)
|
|
133
|
+
.set({
|
|
134
|
+
name,
|
|
135
|
+
description,
|
|
136
|
+
color: color || existingTeam[0].color,
|
|
137
|
+
updatedAt: new Date(),
|
|
138
|
+
})
|
|
139
|
+
.where(eq(teams.id, id))
|
|
140
|
+
.returning();
|
|
141
|
+
|
|
142
|
+
// Audit log
|
|
143
|
+
await createAuditLog({
|
|
144
|
+
userId: user.id,
|
|
145
|
+
teamId: id,
|
|
146
|
+
action: 'update',
|
|
147
|
+
resourceType: 'team',
|
|
148
|
+
resourceId: id,
|
|
149
|
+
oldValue: { name: existingTeam[0].name, description: existingTeam[0].description, color: existingTeam[0].color },
|
|
150
|
+
newValue: { name, description, color: color || existingTeam[0].color },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return NextResponse.json({ team: updatedTeam });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Update team error:', error);
|
|
156
|
+
return NextResponse.json(
|
|
157
|
+
{ error: 'Internal server error' },
|
|
158
|
+
{ status: 500 }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function DELETE(request: NextRequest) {
|
|
164
|
+
try {
|
|
165
|
+
const user = await getUserFromToken();
|
|
166
|
+
|
|
167
|
+
if (!user) {
|
|
168
|
+
return NextResponse.json(
|
|
169
|
+
{ error: 'Not authenticated' },
|
|
170
|
+
{ status: 401 }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { id } = await request.json();
|
|
175
|
+
|
|
176
|
+
if (!id) {
|
|
177
|
+
return NextResponse.json(
|
|
178
|
+
{ error: 'Team ID is required' },
|
|
179
|
+
{ status: 400 }
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check if user owns the team
|
|
184
|
+
const existingTeam = await db
|
|
185
|
+
.select()
|
|
186
|
+
.from(teams)
|
|
187
|
+
.where(eq(teams.id, id))
|
|
188
|
+
.limit(1);
|
|
189
|
+
|
|
190
|
+
if (!existingTeam.length || existingTeam[0].ownerId !== user.id) {
|
|
191
|
+
return NextResponse.json(
|
|
192
|
+
{ error: 'Team not found or access denied' },
|
|
193
|
+
{ status: 404 }
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await db.delete(teams).where(eq(teams.id, id));
|
|
198
|
+
|
|
199
|
+
// Audit log
|
|
200
|
+
await createAuditLog({
|
|
201
|
+
userId: user.id,
|
|
202
|
+
teamId: id,
|
|
203
|
+
action: 'delete',
|
|
204
|
+
resourceType: 'team',
|
|
205
|
+
resourceId: id,
|
|
206
|
+
oldValue: { name: existingTeam[0].name, description: existingTeam[0].description },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return NextResponse.json({ success: true });
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error('Delete team error:', error);
|
|
212
|
+
return NextResponse.json(
|
|
213
|
+
{ error: 'Internal server error' },
|
|
214
|
+
{ status: 500 }
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getUserFromToken } from '@/lib/auth';
|
|
3
|
+
import { db } from '@/lib/db';
|
|
4
|
+
import { variableHistory, teams, environmentVariables, users, teamMembers } from '@/lib/schema';
|
|
5
|
+
import { eq, and, desc, or, exists } from 'drizzle-orm';
|
|
6
|
+
import { decrypt } from '@/lib/crypto';
|
|
7
|
+
import { hasPermission, Permissions } from '@/lib/permissions';
|
|
8
|
+
|
|
9
|
+
export async function GET(request: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const user = await getUserFromToken();
|
|
12
|
+
|
|
13
|
+
if (!user) {
|
|
14
|
+
return NextResponse.json(
|
|
15
|
+
{ error: 'Not authenticated' },
|
|
16
|
+
{ status: 401 }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { searchParams } = new URL(request.url);
|
|
21
|
+
const variableId = searchParams.get('variableId');
|
|
22
|
+
const teamId = searchParams.get('teamId');
|
|
23
|
+
|
|
24
|
+
if (!variableId && !teamId) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: 'Variable ID or Team ID is required' },
|
|
27
|
+
{ status: 400 }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let whereCondition;
|
|
32
|
+
if (variableId) {
|
|
33
|
+
// Check if user has access to this variable
|
|
34
|
+
const variableCheck = await db
|
|
35
|
+
.select()
|
|
36
|
+
.from(environmentVariables)
|
|
37
|
+
.innerJoin(teams, eq(environmentVariables.teamId, teams.id))
|
|
38
|
+
.where(
|
|
39
|
+
and(
|
|
40
|
+
eq(environmentVariables.id, variableId),
|
|
41
|
+
or(
|
|
42
|
+
eq(teams.ownerId, user.id),
|
|
43
|
+
exists(
|
|
44
|
+
db.select()
|
|
45
|
+
.from(teamMembers)
|
|
46
|
+
.where(and(eq(teamMembers.teamId, teams.id), eq(teamMembers.userId, user.id)))
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
.limit(1);
|
|
52
|
+
|
|
53
|
+
if (!variableCheck.length) {
|
|
54
|
+
return NextResponse.json(
|
|
55
|
+
{ error: 'Variable not found or access denied' },
|
|
56
|
+
{ status: 404 }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
whereCondition = eq(variableHistory.variableId, variableId);
|
|
60
|
+
} else {
|
|
61
|
+
// Check if user owns or is a member of the team
|
|
62
|
+
if (!teamId) {
|
|
63
|
+
return NextResponse.json(
|
|
64
|
+
{ error: 'Team ID is required' },
|
|
65
|
+
{ status: 400 }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const teamCheck = await db
|
|
70
|
+
.select()
|
|
71
|
+
.from(teams)
|
|
72
|
+
.where(
|
|
73
|
+
and(
|
|
74
|
+
eq(teams.id, teamId),
|
|
75
|
+
or(
|
|
76
|
+
eq(teams.ownerId, user.id),
|
|
77
|
+
exists(
|
|
78
|
+
db.select()
|
|
79
|
+
.from(teamMembers)
|
|
80
|
+
.where(and(eq(teamMembers.teamId, teams.id), eq(teamMembers.userId, user.id)))
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
.limit(1);
|
|
86
|
+
|
|
87
|
+
if (!teamCheck.length) {
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{ error: 'Team not found or access denied' },
|
|
90
|
+
{ status: 403 }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
whereCondition = eq(variableHistory.teamId, teamId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Get variable history with user details
|
|
97
|
+
const history = await db
|
|
98
|
+
.select({
|
|
99
|
+
id: variableHistory.id,
|
|
100
|
+
variableId: variableHistory.variableId,
|
|
101
|
+
key: variableHistory.key,
|
|
102
|
+
value: variableHistory.value,
|
|
103
|
+
description: variableHistory.description,
|
|
104
|
+
isSecret: variableHistory.isSecret,
|
|
105
|
+
changeType: variableHistory.changeType,
|
|
106
|
+
createdAt: variableHistory.createdAt,
|
|
107
|
+
changer: {
|
|
108
|
+
id: users.id,
|
|
109
|
+
name: users.name,
|
|
110
|
+
email: users.email,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
.from(variableHistory)
|
|
114
|
+
.innerJoin(users, eq(variableHistory.changedBy, users.id))
|
|
115
|
+
.where(whereCondition)
|
|
116
|
+
.orderBy(desc(variableHistory.createdAt))
|
|
117
|
+
.limit(100); // Limit to last 100 changes
|
|
118
|
+
|
|
119
|
+
// Decrypt history values
|
|
120
|
+
const decryptedHistory = history.map(h => ({
|
|
121
|
+
...h,
|
|
122
|
+
value: decrypt(h.value)
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
return NextResponse.json({ history: decryptedHistory });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Get variable history error:', error);
|
|
128
|
+
return NextResponse.json(
|
|
129
|
+
{ error: 'Internal server error' },
|
|
130
|
+
{ status: 500 }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function POST(request: NextRequest) {
|
|
135
|
+
try {
|
|
136
|
+
const user = await getUserFromToken();
|
|
137
|
+
|
|
138
|
+
if (!user) {
|
|
139
|
+
return NextResponse.json(
|
|
140
|
+
{ error: 'Not authenticated' },
|
|
141
|
+
{ status: 401 }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { historyId } = await request.json();
|
|
146
|
+
|
|
147
|
+
if (!historyId) {
|
|
148
|
+
return NextResponse.json(
|
|
149
|
+
{ error: 'History ID is required' },
|
|
150
|
+
{ status: 400 }
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Get the history entry
|
|
155
|
+
const [historyEntry] = await db
|
|
156
|
+
.select()
|
|
157
|
+
.from(variableHistory)
|
|
158
|
+
.where(eq(variableHistory.id, historyId))
|
|
159
|
+
.limit(1);
|
|
160
|
+
|
|
161
|
+
if (!historyEntry) {
|
|
162
|
+
return NextResponse.json(
|
|
163
|
+
{ error: 'History entry not found' },
|
|
164
|
+
{ status: 404 }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Verify user has permission to manage secrets in this team
|
|
169
|
+
const canManage = await hasPermission(user.id, historyEntry.teamId, Permissions.MANAGE_SECRETS);
|
|
170
|
+
|
|
171
|
+
if (!canManage) {
|
|
172
|
+
return NextResponse.json(
|
|
173
|
+
{ error: 'Forbidden: You do not have permission to manage secrets in this project' },
|
|
174
|
+
{ status: 403 }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Roll back the variable
|
|
179
|
+
const [updatedVariable] = await db
|
|
180
|
+
.update(environmentVariables)
|
|
181
|
+
.set({
|
|
182
|
+
value: historyEntry.value, // It's already encrypted in history
|
|
183
|
+
description: historyEntry.description,
|
|
184
|
+
isSecret: historyEntry.isSecret,
|
|
185
|
+
updatedBy: user.id,
|
|
186
|
+
updatedAt: new Date(),
|
|
187
|
+
})
|
|
188
|
+
.where(eq(environmentVariables.id, historyEntry.variableId))
|
|
189
|
+
.returning();
|
|
190
|
+
|
|
191
|
+
// Create a new history entry for the rollback
|
|
192
|
+
await db.insert(variableHistory).values({
|
|
193
|
+
variableId: historyEntry.variableId,
|
|
194
|
+
teamId: historyEntry.teamId,
|
|
195
|
+
environmentId: historyEntry.environmentId,
|
|
196
|
+
key: historyEntry.key,
|
|
197
|
+
value: historyEntry.value, // Already encrypted
|
|
198
|
+
description: historyEntry.description,
|
|
199
|
+
isSecret: historyEntry.isSecret,
|
|
200
|
+
changedBy: user.id,
|
|
201
|
+
changeType: 'update',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return NextResponse.json({
|
|
205
|
+
message: 'Variable rolled back successfully',
|
|
206
|
+
variable: {
|
|
207
|
+
...updatedVariable,
|
|
208
|
+
value: decrypt(updatedVariable.value)
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error('Rollback variable error:', error);
|
|
213
|
+
return NextResponse.json(
|
|
214
|
+
{ error: 'Internal server error' },
|
|
215
|
+
{ status: 500 }
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|