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,227 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getUserFromToken } from '@/lib/auth';
3
+ import { db } from '@/lib/db';
4
+ import { environments, teams, teamMembers } from '@/lib/schema';
5
+ import { eq, and, or, exists } from 'drizzle-orm';
6
+ import { hasPermission, Permissions } from '@/lib/permissions';
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 environments for teams owned by or shared with the user
20
+ const userEnvironments = await db
21
+ .select({
22
+ id: environments.id,
23
+ name: environments.name,
24
+ description: environments.description,
25
+ teamId: environments.teamId,
26
+ parentId: environments.parentId,
27
+ color: environments.color,
28
+ isDefault: environments.isDefault,
29
+ createdAt: environments.createdAt,
30
+ teamName: teams.name,
31
+ })
32
+ .from(environments)
33
+ .innerJoin(teams, eq(environments.teamId, teams.id))
34
+ .where(
35
+ or(
36
+ eq(teams.ownerId, user.id),
37
+ exists(
38
+ db.select()
39
+ .from(teamMembers)
40
+ .where(and(eq(teamMembers.teamId, teams.id), eq(teamMembers.userId, user.id)))
41
+ )
42
+ )
43
+ );
44
+
45
+ return NextResponse.json({ environments: userEnvironments });
46
+ } catch (error) {
47
+ console.error('Get environments error:', error);
48
+ return NextResponse.json(
49
+ { error: 'Internal server error' },
50
+ { status: 500 }
51
+ );
52
+ }
53
+ }
54
+
55
+ export async function POST(request: NextRequest) {
56
+ try {
57
+ const user = await getUserFromToken();
58
+
59
+ if (!user) {
60
+ return NextResponse.json(
61
+ { error: 'Not authenticated' },
62
+ { status: 401 }
63
+ );
64
+ }
65
+
66
+ const { name, description, teamId, color, isDefault, parentId } = await request.json();
67
+
68
+ if (!name || !teamId) {
69
+ return NextResponse.json(
70
+ { error: 'Name and teamId are required' },
71
+ { status: 400 }
72
+ );
73
+ }
74
+
75
+ // Verify user has permission to manage projects/environments
76
+ const canManage = await hasPermission(user.id, teamId, Permissions.MANAGE_TEAM);
77
+
78
+ if (!canManage) {
79
+ return NextResponse.json(
80
+ { error: 'Forbidden: You do not have permission to manage environments in this project' },
81
+ { status: 403 }
82
+ );
83
+ }
84
+
85
+ const [newEnvironment] = await db.insert(environments).values({
86
+ name,
87
+ description,
88
+ teamId,
89
+ parentId: (parentId === 'none' || !parentId) ? null : parentId,
90
+ color: color || '#3b82f6',
91
+ isDefault: isDefault || false,
92
+ }).returning();
93
+
94
+ return NextResponse.json({ environment: newEnvironment });
95
+ } catch (error) {
96
+ console.error('Create environment error:', error);
97
+ return NextResponse.json(
98
+ { error: 'Internal server error' },
99
+ { status: 500 }
100
+ );
101
+ }
102
+ }
103
+
104
+ export async function PUT(request: NextRequest) {
105
+ try {
106
+ const user = await getUserFromToken();
107
+
108
+ if (!user) {
109
+ return NextResponse.json(
110
+ { error: 'Not authenticated' },
111
+ { status: 401 }
112
+ );
113
+ }
114
+
115
+ const { id, name, description, color, isDefault } = await request.json();
116
+
117
+ if (!id) {
118
+ return NextResponse.json(
119
+ { error: 'Environment ID is required' },
120
+ { status: 400 }
121
+ );
122
+ }
123
+
124
+ if (!name) {
125
+ return NextResponse.json(
126
+ { error: 'Name is required' },
127
+ { status: 400 }
128
+ );
129
+ }
130
+
131
+ // Check for managing permissions
132
+ const env = await db
133
+ .select({ teamId: environments.teamId })
134
+ .from(environments)
135
+ .where(eq(environments.id, id))
136
+ .limit(1);
137
+
138
+ if (!env.length) {
139
+ return NextResponse.json(
140
+ { error: 'Environment not found' },
141
+ { status: 404 }
142
+ );
143
+ }
144
+
145
+ const canManage = await hasPermission(user.id, env[0].teamId, Permissions.MANAGE_TEAM);
146
+ if (!canManage) {
147
+ return NextResponse.json(
148
+ { error: 'Forbidden: You do not have permission to manage environments in this project' },
149
+ { status: 403 }
150
+ );
151
+ }
152
+
153
+ const [updatedEnvironment] = await db
154
+ .update(environments)
155
+ .set({
156
+ name,
157
+ description,
158
+ color: color || '#3b82f6',
159
+ isDefault: isDefault !== undefined ? isDefault : false,
160
+ updatedAt: new Date(),
161
+ })
162
+ .where(eq(environments.id, id))
163
+ .returning();
164
+
165
+ return NextResponse.json({ environment: updatedEnvironment });
166
+ } catch (error) {
167
+ console.error('Update environment error:', error);
168
+ return NextResponse.json(
169
+ { error: 'Internal server error' },
170
+ { status: 500 }
171
+ );
172
+ }
173
+ }
174
+
175
+ export async function DELETE(request: NextRequest) {
176
+ try {
177
+ const user = await getUserFromToken();
178
+
179
+ if (!user) {
180
+ return NextResponse.json(
181
+ { error: 'Not authenticated' },
182
+ { status: 401 }
183
+ );
184
+ }
185
+
186
+ const { id } = await request.json();
187
+
188
+ if (!id) {
189
+ return NextResponse.json(
190
+ { error: 'Environment ID is required' },
191
+ { status: 400 }
192
+ );
193
+ }
194
+
195
+ // Check for managing permissions
196
+ const env = await db
197
+ .select({ teamId: environments.teamId })
198
+ .from(environments)
199
+ .where(eq(environments.id, id))
200
+ .limit(1);
201
+
202
+ if (!env.length) {
203
+ return NextResponse.json(
204
+ { error: 'Environment not found' },
205
+ { status: 404 }
206
+ );
207
+ }
208
+
209
+ const canManage = await hasPermission(user.id, env[0].teamId, Permissions.MANAGE_TEAM);
210
+ if (!canManage) {
211
+ return NextResponse.json(
212
+ { error: 'Forbidden: You do not have permission to delete environments in this project' },
213
+ { status: 403 }
214
+ );
215
+ }
216
+
217
+ await db.delete(environments).where(eq(environments.id, id));
218
+
219
+ return NextResponse.json({ success: true });
220
+ } catch (error) {
221
+ console.error('Delete environment error:', error);
222
+ return NextResponse.json(
223
+ { error: 'Internal server error' },
224
+ { status: 500 }
225
+ );
226
+ }
227
+ }
@@ -0,0 +1,385 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { eq, and } from 'drizzle-orm';
3
+ import { getUserFromToken } from '@/lib/auth';
4
+ import { db } from '@/lib/db';
5
+ import { teamMembers, teams, users, invitations } from '@/lib/schema';
6
+ import { createAuditLog } from '@/lib/audit';
7
+ import { hasPermission, Permissions } from '@/lib/permissions';
8
+ import { sendEmail, emailTemplates } from '@/lib/email';
9
+ import crypto from 'crypto';
10
+
11
+ export async function GET(request: NextRequest) {
12
+ try {
13
+ const user = await getUserFromToken();
14
+
15
+ if (!user) {
16
+ return NextResponse.json(
17
+ { error: 'Not authenticated' },
18
+ { status: 401 }
19
+ );
20
+ }
21
+
22
+ const { searchParams } = new URL(request.url);
23
+ const teamId = searchParams.get('teamId');
24
+
25
+ if (!teamId) {
26
+ return NextResponse.json(
27
+ { error: 'Team ID is required' },
28
+ { status: 400 }
29
+ );
30
+ }
31
+
32
+ // Check if user is a member of the team or owner
33
+ const teamCheck = await db
34
+ .select()
35
+ .from(teams)
36
+ .where(and(eq(teams.id, teamId), eq(teams.ownerId, user.id)))
37
+ .limit(1);
38
+
39
+ const memberCheck = await db
40
+ .select()
41
+ .from(teamMembers)
42
+ .where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, user.id)))
43
+ .limit(1);
44
+
45
+ if (!teamCheck.length && !memberCheck.length) {
46
+ return NextResponse.json(
47
+ { error: 'Access denied' },
48
+ { status: 403 }
49
+ );
50
+ }
51
+
52
+ // Get team members with user details
53
+ const members = await db
54
+ .select({
55
+ id: teamMembers.id,
56
+ teamId: teamMembers.teamId,
57
+ userId: teamMembers.userId,
58
+ role: teamMembers.role,
59
+ joinedAt: teamMembers.joinedAt,
60
+ user: {
61
+ id: users.id,
62
+ name: users.name,
63
+ email: users.email,
64
+ avatar: users.avatar,
65
+ },
66
+ })
67
+ .from(teamMembers)
68
+ .innerJoin(users, eq(teamMembers.userId, users.id))
69
+ .where(eq(teamMembers.teamId, teamId));
70
+
71
+ return NextResponse.json({ members });
72
+ } catch (error) {
73
+ console.error('Get team members error:', error);
74
+ return NextResponse.json(
75
+ { error: 'Internal server error' },
76
+ { status: 500 }
77
+ );
78
+ }
79
+ }
80
+
81
+ export async function POST(request: NextRequest) {
82
+ try {
83
+ const user = await getUserFromToken();
84
+
85
+ if (!user) {
86
+ return NextResponse.json(
87
+ { error: 'Not authenticated' },
88
+ { status: 401 }
89
+ );
90
+ }
91
+
92
+ const { teamId, email, role } = await request.json();
93
+
94
+ if (!teamId || !email || !role) {
95
+ return NextResponse.json(
96
+ { error: 'Team ID, email, and role are required' },
97
+ { status: 400 }
98
+ );
99
+ }
100
+
101
+ // Check if user has permission to manage team
102
+ const canManageTeam = await hasPermission(user.id, teamId, Permissions.MANAGE_TEAM);
103
+
104
+ if (!canManageTeam) {
105
+ return NextResponse.json(
106
+ { error: 'Forbidden: You do not have permission to manage team members' },
107
+ { status: 403 }
108
+ );
109
+ }
110
+
111
+ // Get team details for the email
112
+ const [team] = await db
113
+ .select()
114
+ .from(teams)
115
+ .where(eq(teams.id, teamId))
116
+ .limit(1);
117
+
118
+ if (!team) {
119
+ return NextResponse.json({ error: 'Team not found' }, { status: 404 });
120
+ }
121
+
122
+ // Check if user exists
123
+ const [targetUser] = await db
124
+ .select()
125
+ .from(users)
126
+ .where(eq(users.email, email))
127
+ .limit(1);
128
+
129
+ if (targetUser) {
130
+ // Check if user is already a member
131
+ const existingMember = await db
132
+ .select()
133
+ .from(teamMembers)
134
+ .where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, targetUser.id)))
135
+ .limit(1);
136
+
137
+ if (existingMember.length) {
138
+ return NextResponse.json(
139
+ { error: 'User is already a member of this team' },
140
+ { status: 400 }
141
+ );
142
+ }
143
+
144
+ // Add member to team directly
145
+ const [newMember] = await db.insert(teamMembers).values({
146
+ teamId,
147
+ userId: targetUser.id,
148
+ role,
149
+ }).returning();
150
+
151
+ // Audit log
152
+ await createAuditLog({
153
+ userId: user.id,
154
+ teamId,
155
+ action: 'create',
156
+ resourceType: 'member',
157
+ resourceId: newMember.id,
158
+ newValue: { userId: targetUser.id, role, email: targetUser.email },
159
+ });
160
+
161
+ // Send notification email
162
+ try {
163
+ const template = emailTemplates.teamInvitation(user.name, team.name, teamId, ''); // No token needed if they exist, or just link to dashboard
164
+ await sendEmail({
165
+ to: email,
166
+ subject: `You've been added to ${team.name}`,
167
+ html: template.html,
168
+ });
169
+ } catch (mailError) {
170
+ console.error('Failed to send notification email:', mailError);
171
+ // Don't fail the request if email fails
172
+ }
173
+
174
+ return NextResponse.json({ member: newMember });
175
+ } else {
176
+ // User doesn't exist, create an invitation
177
+ const token = crypto.randomBytes(32).toString('hex');
178
+ const expiresAt = new Date();
179
+ expiresAt.setDate(expiresAt.getDate() + 7); // Invite expires in 7 days
180
+
181
+ // Check if there's already a pending invitation for this email and team
182
+ const [existingInvite] = await db
183
+ .select()
184
+ .from(invitations)
185
+ .where(and(
186
+ eq(invitations.teamId, teamId),
187
+ eq(invitations.email, email),
188
+ eq(invitations.status, 'pending')
189
+ ))
190
+ .limit(1);
191
+
192
+ if (existingInvite) {
193
+ // Update existing invite
194
+ await db.update(invitations)
195
+ .set({ token, expiresAt, role, updatedAt: new Date() })
196
+ .where(eq(invitations.id, existingInvite.id));
197
+ } else {
198
+ // Create new invite
199
+ await db.insert(invitations).values({
200
+ teamId,
201
+ email,
202
+ role,
203
+ invitedBy: user.id,
204
+ token,
205
+ expiresAt,
206
+ });
207
+ }
208
+
209
+ // Send invitation email
210
+ try {
211
+ const template = emailTemplates.teamInvitation(user.name, team.name, teamId, token);
212
+ await sendEmail({ to: email, ...template });
213
+ } catch (mailError) {
214
+ console.error('Failed to send invitation email:', mailError);
215
+ return NextResponse.json(
216
+ { error: 'Failed to send invitation email. Please check SMTP settings.' },
217
+ { status: 500 }
218
+ );
219
+ }
220
+
221
+ return NextResponse.json({ message: 'Invitation sent' });
222
+ }
223
+ } catch (error) {
224
+ console.error('Invite team member error:', error);
225
+ return NextResponse.json(
226
+ { error: 'Internal server error' },
227
+ { status: 500 }
228
+ );
229
+ }
230
+ }
231
+
232
+ export async function PUT(request: NextRequest) {
233
+ try {
234
+ const user = await getUserFromToken();
235
+
236
+ if (!user) {
237
+ return NextResponse.json(
238
+ { error: 'Not authenticated' },
239
+ { status: 401 }
240
+ );
241
+ }
242
+
243
+ const { teamId, memberId, role } = await request.json();
244
+
245
+ if (!memberId || !role) {
246
+ return NextResponse.json(
247
+ { error: 'Member ID and role are required' },
248
+ { status: 400 }
249
+ );
250
+ }
251
+
252
+ // Get member details
253
+ const [member] = await db
254
+ .select()
255
+ .from(teamMembers)
256
+ .where(eq(teamMembers.id, memberId))
257
+ .limit(1);
258
+
259
+ if (!member) {
260
+ return NextResponse.json(
261
+ { error: 'Member not found' },
262
+ { status: 404 }
263
+ );
264
+ }
265
+
266
+ // Check if user has permission to manage team
267
+ const targetTeamId = teamId || member.teamId;
268
+ const canManageTeam = await hasPermission(user.id, targetTeamId, Permissions.MANAGE_TEAM);
269
+
270
+ if (!canManageTeam) {
271
+ return NextResponse.json(
272
+ { error: 'Forbidden: You do not have permission to manage team members' },
273
+ { status: 403 }
274
+ );
275
+ }
276
+
277
+ // Update member role
278
+ const [updatedMember] = await db
279
+ .update(teamMembers)
280
+ .set({ role })
281
+ .where(eq(teamMembers.id, memberId))
282
+ .returning();
283
+
284
+ // Audit log
285
+ await createAuditLog({
286
+ userId: user.id,
287
+ teamId: member.teamId,
288
+ action: 'update',
289
+ resourceType: 'member',
290
+ resourceId: memberId,
291
+ oldValue: { role: member.role },
292
+ newValue: { role },
293
+ });
294
+
295
+ return NextResponse.json({ member: updatedMember });
296
+ } catch (error) {
297
+ console.error('Update team member error:', error);
298
+ return NextResponse.json(
299
+ { error: 'Internal server error' },
300
+ { status: 500 }
301
+ );
302
+ }
303
+ }
304
+
305
+ export async function DELETE(request: NextRequest) {
306
+ try {
307
+ const user = await getUserFromToken();
308
+
309
+ if (!user) {
310
+ return NextResponse.json(
311
+ { error: 'Not authenticated' },
312
+ { status: 401 }
313
+ );
314
+ }
315
+
316
+ const { teamId, memberId } = await request.json();
317
+
318
+ if (!memberId) {
319
+ return NextResponse.json(
320
+ { error: 'Member ID is required' },
321
+ { status: 400 }
322
+ );
323
+ }
324
+
325
+ // Get member details
326
+ const [member] = await db
327
+ .select()
328
+ .from(teamMembers)
329
+ .where(eq(teamMembers.id, memberId))
330
+ .limit(1);
331
+
332
+ if (!member) {
333
+ return NextResponse.json(
334
+ { error: 'Member not found' },
335
+ { status: 404 }
336
+ );
337
+ }
338
+
339
+ // Check if user has permission to manage team (or if they are removing themselves)
340
+ const targetTeamId = teamId || member.teamId;
341
+ const isRemovingSelf = member.userId === user.id;
342
+ const canManageTeam = await hasPermission(user.id, targetTeamId, Permissions.MANAGE_TEAM);
343
+
344
+ if (!canManageTeam && !isRemovingSelf) {
345
+ return NextResponse.json(
346
+ { error: 'Forbidden: You do not have permission to manage team members' },
347
+ { status: 403 }
348
+ );
349
+ }
350
+
351
+ // Can't remove yourself if you're the owner
352
+ const [team] = await db
353
+ .select()
354
+ .from(teams)
355
+ .where(eq(teams.id, targetTeamId))
356
+ .limit(1);
357
+
358
+ if (member.userId === user.id && team?.ownerId === user.id) {
359
+ return NextResponse.json(
360
+ { error: 'Cannot remove yourself as team owner' },
361
+ { status: 400 }
362
+ );
363
+ }
364
+
365
+ await db.delete(teamMembers).where(eq(teamMembers.id, memberId));
366
+
367
+ // Audit log
368
+ await createAuditLog({
369
+ userId: user.id,
370
+ teamId: member.teamId,
371
+ action: 'delete',
372
+ resourceType: 'member',
373
+ resourceId: memberId,
374
+ oldValue: { userId: member.userId, role: member.role },
375
+ });
376
+
377
+ return NextResponse.json({ success: true });
378
+ } catch (error) {
379
+ console.error('Remove team member error:', error);
380
+ return NextResponse.json(
381
+ { error: 'Internal server error' },
382
+ { status: 500 }
383
+ );
384
+ }
385
+ }