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,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
|
+
}
|