@chaaskit/server 0.1.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/dist/api/admin.js +438 -0
- package/dist/api/admin.js.map +1 -0
- package/dist/api/agents.js +21 -0
- package/dist/api/agents.js.map +1 -0
- package/dist/api/api-keys.js +122 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/auth.js +399 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/chat.js +900 -0
- package/dist/api/chat.js.map +1 -0
- package/dist/api/config.js +91 -0
- package/dist/api/config.js.map +1 -0
- package/dist/api/documents.js +237 -0
- package/dist/api/documents.js.map +1 -0
- package/dist/api/export.js +107 -0
- package/dist/api/export.js.map +1 -0
- package/dist/api/health.js +25 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/mcp-server.js +84 -0
- package/dist/api/mcp-server.js.map +1 -0
- package/dist/api/mcp.js +400 -0
- package/dist/api/mcp.js.map +1 -0
- package/dist/api/mentions.js +94 -0
- package/dist/api/mentions.js.map +1 -0
- package/dist/api/oauth.js +366 -0
- package/dist/api/oauth.js.map +1 -0
- package/dist/api/payments.js +473 -0
- package/dist/api/payments.js.map +1 -0
- package/dist/api/projects.js +301 -0
- package/dist/api/projects.js.map +1 -0
- package/dist/api/scheduled-prompts.js +617 -0
- package/dist/api/scheduled-prompts.js.map +1 -0
- package/dist/api/search.js +85 -0
- package/dist/api/search.js.map +1 -0
- package/dist/api/share.js +188 -0
- package/dist/api/share.js.map +1 -0
- package/dist/api/slack.js +468 -0
- package/dist/api/slack.js.map +1 -0
- package/dist/api/teams.js +693 -0
- package/dist/api/teams.js.map +1 -0
- package/dist/api/templates.js +134 -0
- package/dist/api/templates.js.map +1 -0
- package/dist/api/threads.js +323 -0
- package/dist/api/threads.js.map +1 -0
- package/dist/api/upload.js +57 -0
- package/dist/api/upload.js.map +1 -0
- package/dist/api/user.js +111 -0
- package/dist/api/user.js.map +1 -0
- package/dist/api/v1/openai.js +245 -0
- package/dist/api/v1/openai.js.map +1 -0
- package/dist/app.js +168 -0
- package/dist/app.js.map +1 -0
- package/dist/bin/cli.js +57 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/commands/db-sync.js +108 -0
- package/dist/commands/db-sync.js.map +1 -0
- package/dist/config/loader.js +374 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/documents/extractors.js +136 -0
- package/dist/documents/extractors.js.map +1 -0
- package/dist/extensions/glob.js +53 -0
- package/dist/extensions/glob.js.map +1 -0
- package/dist/extensions/loader.js +72 -0
- package/dist/extensions/loader.js.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/loaders/index.js +75 -0
- package/dist/loaders/index.js.map +1 -0
- package/dist/mcp/client.js +551 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/server.js +335 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/middleware/apiKeyAuth.js +136 -0
- package/dist/middleware/apiKeyAuth.js.map +1 -0
- package/dist/middleware/auth.js +192 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/errorHandler.js +41 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/mcpServerAuth.js +164 -0
- package/dist/middleware/mcpServerAuth.js.map +1 -0
- package/dist/middleware/requestLogger.js +9 -0
- package/dist/middleware/requestLogger.js.map +1 -0
- package/dist/middleware/team.js +132 -0
- package/dist/middleware/team.js.map +1 -0
- package/dist/oauth/server.js +410 -0
- package/dist/oauth/server.js.map +1 -0
- package/dist/queue/cli.js +93 -0
- package/dist/queue/cli.js.map +1 -0
- package/dist/queue/handlers/index.js +91 -0
- package/dist/queue/handlers/index.js.map +1 -0
- package/dist/queue/handlers/scheduled-prompt.js +270 -0
- package/dist/queue/handlers/scheduled-prompt.js.map +1 -0
- package/dist/queue/index.js +91 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/providers/memory.js +296 -0
- package/dist/queue/providers/memory.js.map +1 -0
- package/dist/queue/providers/sqs.js +275 -0
- package/dist/queue/providers/sqs.js.map +1 -0
- package/dist/queue/scheduler.js +355 -0
- package/dist/queue/scheduler.js.map +1 -0
- package/dist/queue/types.js +5 -0
- package/dist/queue/types.js.map +1 -0
- package/dist/queue/worker.js +230 -0
- package/dist/queue/worker.js.map +1 -0
- package/dist/registry/index.js +40 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/server.js +207 -0
- package/dist/server.js.map +1 -0
- package/dist/services/agent.js +530 -0
- package/dist/services/agent.js.map +1 -0
- package/dist/services/agents.js +194 -0
- package/dist/services/agents.js.map +1 -0
- package/dist/services/documents.js +507 -0
- package/dist/services/documents.js.map +1 -0
- package/dist/services/email/index.js +91 -0
- package/dist/services/email/index.js.map +1 -0
- package/dist/services/email/providers/ses.js +97 -0
- package/dist/services/email/providers/ses.js.map +1 -0
- package/dist/services/email/templates.js +194 -0
- package/dist/services/email/templates.js.map +1 -0
- package/dist/services/email/types.js +5 -0
- package/dist/services/email/types.js.map +1 -0
- package/dist/services/encryption.js +69 -0
- package/dist/services/encryption.js.map +1 -0
- package/dist/services/oauth-discovery.js +226 -0
- package/dist/services/oauth-discovery.js.map +1 -0
- package/dist/services/pendingConfirmation.js +105 -0
- package/dist/services/pendingConfirmation.js.map +1 -0
- package/dist/services/scheduledPrompts.js +70 -0
- package/dist/services/scheduledPrompts.js.map +1 -0
- package/dist/services/slack/client.js +174 -0
- package/dist/services/slack/client.js.map +1 -0
- package/dist/services/slack/events.js +189 -0
- package/dist/services/slack/events.js.map +1 -0
- package/dist/services/slack/index.js +6 -0
- package/dist/services/slack/index.js.map +1 -0
- package/dist/services/slack/notifications.js +124 -0
- package/dist/services/slack/notifications.js.map +1 -0
- package/dist/services/slack/signature.js +74 -0
- package/dist/services/slack/signature.js.map +1 -0
- package/dist/services/slack/thread-context.js +191 -0
- package/dist/services/slack/thread-context.js.map +1 -0
- package/dist/services/toolConfirmation.js +55 -0
- package/dist/services/toolConfirmation.js.map +1 -0
- package/dist/services/usage.js +241 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/ssr/build.js +90 -0
- package/dist/ssr/build.js.map +1 -0
- package/dist/ssr/components/SSRMessageList.js +120 -0
- package/dist/ssr/components/SSRMessageList.js.map +1 -0
- package/dist/ssr/entry.client.js +8 -0
- package/dist/ssr/entry.client.js.map +1 -0
- package/dist/ssr/entry.server.js +71 -0
- package/dist/ssr/entry.server.js.map +1 -0
- package/dist/ssr/handler.js +51 -0
- package/dist/ssr/handler.js.map +1 -0
- package/dist/ssr/root.js +184 -0
- package/dist/ssr/root.js.map +1 -0
- package/dist/ssr/routes/login.js +140 -0
- package/dist/ssr/routes/login.js.map +1 -0
- package/dist/ssr/routes/pricing.js +195 -0
- package/dist/ssr/routes/pricing.js.map +1 -0
- package/dist/ssr/routes/privacy.js +39 -0
- package/dist/ssr/routes/privacy.js.map +1 -0
- package/dist/ssr/routes/register.js +148 -0
- package/dist/ssr/routes/register.js.map +1 -0
- package/dist/ssr/routes/shared.$shareId.js +153 -0
- package/dist/ssr/routes/shared.$shareId.js.map +1 -0
- package/dist/ssr/routes/terms.js +39 -0
- package/dist/ssr/routes/terms.js.map +1 -0
- package/dist/storage/index.js +43 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/providers/database.js +38 -0
- package/dist/storage/providers/database.js.map +1 -0
- package/dist/storage/providers/filesystem.js +51 -0
- package/dist/storage/providers/filesystem.js.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/tools/documents.js +336 -0
- package/dist/tools/documents.js.map +1 -0
- package/dist/tools/get-plan-usage.js +82 -0
- package/dist/tools/get-plan-usage.js.map +1 -0
- package/dist/tools/index.js +106 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/web-scrape.js +145 -0
- package/dist/tools/web-scrape.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { db } from '@chaaskit/db';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { HTTP_STATUS, createTeamSchema, updateTeamSchema, inviteMemberSchema, updateMemberRoleSchema, } from '@chaaskit/shared';
|
|
5
|
+
import { requireAuth } from '../middleware/auth.js';
|
|
6
|
+
import { requireTeamRole } from '../middleware/team.js';
|
|
7
|
+
import { AppError } from '../middleware/errorHandler.js';
|
|
8
|
+
import { getAgentById } from '../services/agents.js';
|
|
9
|
+
import { getConfig } from '../config/loader.js';
|
|
10
|
+
import { notifyTeamMemberJoined } from '../services/slack/notifications.js';
|
|
11
|
+
import { isEmailEnabled, sendEmail, generateTeamInviteEmailHtml, generateTeamInviteEmailText, } from '../services/email/index.js';
|
|
12
|
+
export const teamsRouter = Router();
|
|
13
|
+
// Middleware to check if teams feature is enabled
|
|
14
|
+
const requireTeamsEnabled = (req, res, next) => {
|
|
15
|
+
const config = getConfig();
|
|
16
|
+
if (!config.teams?.enabled) {
|
|
17
|
+
return next(new AppError(HTTP_STATUS.FORBIDDEN, 'Team workspaces are not enabled'));
|
|
18
|
+
}
|
|
19
|
+
next();
|
|
20
|
+
};
|
|
21
|
+
// Apply teams enabled check to all routes
|
|
22
|
+
teamsRouter.use(requireTeamsEnabled);
|
|
23
|
+
// List user's teams
|
|
24
|
+
teamsRouter.get('/', requireAuth, async (req, res, next) => {
|
|
25
|
+
try {
|
|
26
|
+
const memberships = await db.teamMember.findMany({
|
|
27
|
+
where: { userId: req.user.id },
|
|
28
|
+
include: {
|
|
29
|
+
team: {
|
|
30
|
+
include: {
|
|
31
|
+
_count: {
|
|
32
|
+
select: {
|
|
33
|
+
members: true,
|
|
34
|
+
threads: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const teams = memberships
|
|
42
|
+
.filter((m) => !m.team.archivedAt) // Exclude archived teams
|
|
43
|
+
.map((m) => ({
|
|
44
|
+
id: m.team.id,
|
|
45
|
+
name: m.team.name,
|
|
46
|
+
archivedAt: m.team.archivedAt,
|
|
47
|
+
createdAt: m.team.createdAt,
|
|
48
|
+
updatedAt: m.team.updatedAt,
|
|
49
|
+
role: m.role,
|
|
50
|
+
memberCount: m.team._count.members,
|
|
51
|
+
threadCount: m.team._count.threads,
|
|
52
|
+
}));
|
|
53
|
+
res.json({ teams });
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
next(error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// Create new team
|
|
60
|
+
teamsRouter.post('/', requireAuth, async (req, res, next) => {
|
|
61
|
+
try {
|
|
62
|
+
const { name } = createTeamSchema.parse(req.body);
|
|
63
|
+
const team = await db.team.create({
|
|
64
|
+
data: {
|
|
65
|
+
name,
|
|
66
|
+
members: {
|
|
67
|
+
create: {
|
|
68
|
+
userId: req.user.id,
|
|
69
|
+
role: 'owner',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
include: {
|
|
74
|
+
members: {
|
|
75
|
+
include: {
|
|
76
|
+
user: {
|
|
77
|
+
select: {
|
|
78
|
+
id: true,
|
|
79
|
+
email: true,
|
|
80
|
+
name: true,
|
|
81
|
+
avatarUrl: true,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
invites: true,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const teamDetails = {
|
|
90
|
+
id: team.id,
|
|
91
|
+
name: team.name,
|
|
92
|
+
context: team.context,
|
|
93
|
+
archivedAt: team.archivedAt,
|
|
94
|
+
createdAt: team.createdAt,
|
|
95
|
+
updatedAt: team.updatedAt,
|
|
96
|
+
members: team.members.map((m) => ({
|
|
97
|
+
id: m.id,
|
|
98
|
+
teamId: m.teamId,
|
|
99
|
+
userId: m.userId,
|
|
100
|
+
role: m.role,
|
|
101
|
+
createdAt: m.createdAt,
|
|
102
|
+
user: m.user,
|
|
103
|
+
})),
|
|
104
|
+
invites: team.invites,
|
|
105
|
+
};
|
|
106
|
+
res.status(HTTP_STATUS.CREATED).json({ team: teamDetails });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
next(error);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// Get team details
|
|
113
|
+
teamsRouter.get('/:teamId', requireAuth, requireTeamRole('viewer'), async (req, res, next) => {
|
|
114
|
+
try {
|
|
115
|
+
const { teamId } = req.params;
|
|
116
|
+
const team = await db.team.findUnique({
|
|
117
|
+
where: { id: teamId },
|
|
118
|
+
include: {
|
|
119
|
+
members: {
|
|
120
|
+
include: {
|
|
121
|
+
user: {
|
|
122
|
+
select: {
|
|
123
|
+
id: true,
|
|
124
|
+
email: true,
|
|
125
|
+
name: true,
|
|
126
|
+
avatarUrl: true,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
invites: {
|
|
132
|
+
where: {
|
|
133
|
+
acceptedAt: null,
|
|
134
|
+
expiresAt: { gt: new Date() },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
if (!team) {
|
|
140
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Team not found');
|
|
141
|
+
}
|
|
142
|
+
const teamDetails = {
|
|
143
|
+
id: team.id,
|
|
144
|
+
name: team.name,
|
|
145
|
+
context: team.context,
|
|
146
|
+
archivedAt: team.archivedAt,
|
|
147
|
+
createdAt: team.createdAt,
|
|
148
|
+
updatedAt: team.updatedAt,
|
|
149
|
+
members: team.members.map((m) => ({
|
|
150
|
+
id: m.id,
|
|
151
|
+
teamId: m.teamId,
|
|
152
|
+
userId: m.userId,
|
|
153
|
+
role: m.role,
|
|
154
|
+
createdAt: m.createdAt,
|
|
155
|
+
user: m.user,
|
|
156
|
+
})),
|
|
157
|
+
invites: team.invites,
|
|
158
|
+
};
|
|
159
|
+
res.json({ team: teamDetails });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
next(error);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// Update team
|
|
166
|
+
teamsRouter.patch('/:teamId', requireAuth, requireTeamRole('admin'), async (req, res, next) => {
|
|
167
|
+
try {
|
|
168
|
+
const { teamId } = req.params;
|
|
169
|
+
const { name, context } = updateTeamSchema.parse(req.body);
|
|
170
|
+
const updateData = {};
|
|
171
|
+
if (name !== undefined)
|
|
172
|
+
updateData.name = name;
|
|
173
|
+
if (context !== undefined)
|
|
174
|
+
updateData.context = context;
|
|
175
|
+
const team = await db.team.update({
|
|
176
|
+
where: { id: teamId },
|
|
177
|
+
data: updateData,
|
|
178
|
+
});
|
|
179
|
+
res.json({ team });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
next(error);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// Archive team (owner only)
|
|
186
|
+
teamsRouter.post('/:teamId/archive', requireAuth, requireTeamRole('owner'), async (req, res, next) => {
|
|
187
|
+
try {
|
|
188
|
+
const { teamId } = req.params;
|
|
189
|
+
const team = await db.team.update({
|
|
190
|
+
where: { id: teamId },
|
|
191
|
+
data: { archivedAt: new Date() },
|
|
192
|
+
});
|
|
193
|
+
res.json({ team });
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
next(error);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// Unarchive team (owner only)
|
|
200
|
+
teamsRouter.post('/:teamId/unarchive', requireAuth, requireTeamRole('owner'), async (req, res, next) => {
|
|
201
|
+
try {
|
|
202
|
+
const { teamId } = req.params;
|
|
203
|
+
// Need to bypass the archived check in middleware for unarchiving
|
|
204
|
+
const team = await db.team.findUnique({
|
|
205
|
+
where: { id: teamId },
|
|
206
|
+
});
|
|
207
|
+
if (!team) {
|
|
208
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Team not found');
|
|
209
|
+
}
|
|
210
|
+
const updatedTeam = await db.team.update({
|
|
211
|
+
where: { id: teamId },
|
|
212
|
+
data: { archivedAt: null },
|
|
213
|
+
});
|
|
214
|
+
res.json({ team: updatedTeam });
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
next(error);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
// Invite member (admin+)
|
|
221
|
+
teamsRouter.post('/:teamId/invite', requireAuth, requireTeamRole('admin'), async (req, res, next) => {
|
|
222
|
+
try {
|
|
223
|
+
const { teamId } = req.params;
|
|
224
|
+
const { email, role } = inviteMemberSchema.parse(req.body);
|
|
225
|
+
// Check if user is already a member
|
|
226
|
+
const existingMember = await db.teamMember.findFirst({
|
|
227
|
+
where: {
|
|
228
|
+
teamId,
|
|
229
|
+
user: { email },
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
if (existingMember) {
|
|
233
|
+
throw new AppError(HTTP_STATUS.CONFLICT, 'User is already a member of this team');
|
|
234
|
+
}
|
|
235
|
+
// Check if there's already a pending invite
|
|
236
|
+
const existingInvite = await db.teamInvite.findUnique({
|
|
237
|
+
where: {
|
|
238
|
+
teamId_email: { teamId, email },
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
if (existingInvite && !existingInvite.acceptedAt && existingInvite.expiresAt > new Date()) {
|
|
242
|
+
throw new AppError(HTTP_STATUS.CONFLICT, 'An invite already exists for this email');
|
|
243
|
+
}
|
|
244
|
+
// Delete expired or used invites for this email
|
|
245
|
+
if (existingInvite) {
|
|
246
|
+
await db.teamInvite.delete({
|
|
247
|
+
where: { id: existingInvite.id },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
251
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
|
252
|
+
const invite = await db.teamInvite.create({
|
|
253
|
+
data: {
|
|
254
|
+
teamId,
|
|
255
|
+
email,
|
|
256
|
+
role,
|
|
257
|
+
token,
|
|
258
|
+
invitedBy: req.user.id,
|
|
259
|
+
expiresAt,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
const config = getConfig();
|
|
263
|
+
const appUrl = process.env.APP_URL || 'http://localhost:5173';
|
|
264
|
+
const inviteUrl = `${appUrl}/invite/${token}`;
|
|
265
|
+
// Get team name for email
|
|
266
|
+
const team = await db.team.findUnique({
|
|
267
|
+
where: { id: teamId },
|
|
268
|
+
select: { name: true },
|
|
269
|
+
});
|
|
270
|
+
// Send invitation email if email is enabled
|
|
271
|
+
let emailSent = false;
|
|
272
|
+
if (isEmailEnabled() && team) {
|
|
273
|
+
const inviterName = req.user.name || null;
|
|
274
|
+
const html = generateTeamInviteEmailHtml(team.name, inviteUrl, inviterName, config);
|
|
275
|
+
const text = generateTeamInviteEmailText(team.name, inviteUrl, inviterName, config);
|
|
276
|
+
try {
|
|
277
|
+
const result = await sendEmail({
|
|
278
|
+
to: email,
|
|
279
|
+
subject: `You're invited to join ${team.name} on ${config.app.name}`,
|
|
280
|
+
html,
|
|
281
|
+
text,
|
|
282
|
+
});
|
|
283
|
+
emailSent = !!result;
|
|
284
|
+
if (result) {
|
|
285
|
+
console.log(`[Teams] Invitation email sent to ${email} (messageId: ${result.messageId})`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
console.error(`[Teams] Failed to send invitation email to ${email}:`, err);
|
|
290
|
+
// Continue even if email fails - the invite is still created
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Email disabled - log the invite URL for development
|
|
295
|
+
console.log(`[Teams] Email disabled - invitation URL for ${email}: ${inviteUrl}`);
|
|
296
|
+
}
|
|
297
|
+
res.status(HTTP_STATUS.CREATED).json({
|
|
298
|
+
invite: invite,
|
|
299
|
+
inviteUrl,
|
|
300
|
+
emailSent,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
next(error);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
// Get invite details (public - for invite acceptance page)
|
|
308
|
+
teamsRouter.get('/invite/:token', async (req, res, next) => {
|
|
309
|
+
try {
|
|
310
|
+
const { token } = req.params;
|
|
311
|
+
const invite = await db.teamInvite.findUnique({
|
|
312
|
+
where: { token },
|
|
313
|
+
include: {
|
|
314
|
+
team: {
|
|
315
|
+
select: {
|
|
316
|
+
id: true,
|
|
317
|
+
name: true,
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
if (!invite) {
|
|
323
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Invite not found');
|
|
324
|
+
}
|
|
325
|
+
if (invite.acceptedAt) {
|
|
326
|
+
throw new AppError(HTTP_STATUS.GONE, 'Invite has already been used');
|
|
327
|
+
}
|
|
328
|
+
if (invite.expiresAt < new Date()) {
|
|
329
|
+
throw new AppError(HTTP_STATUS.GONE, 'Invite has expired');
|
|
330
|
+
}
|
|
331
|
+
res.json({
|
|
332
|
+
invite: {
|
|
333
|
+
email: invite.email,
|
|
334
|
+
role: invite.role,
|
|
335
|
+
teamName: invite.team.name,
|
|
336
|
+
expiresAt: invite.expiresAt,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
next(error);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
// Accept invite
|
|
345
|
+
teamsRouter.post('/invite/:token/accept', requireAuth, async (req, res, next) => {
|
|
346
|
+
try {
|
|
347
|
+
const { token } = req.params;
|
|
348
|
+
const invite = await db.teamInvite.findUnique({
|
|
349
|
+
where: { token },
|
|
350
|
+
include: {
|
|
351
|
+
team: true,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
if (!invite) {
|
|
355
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Invite not found');
|
|
356
|
+
}
|
|
357
|
+
if (invite.acceptedAt) {
|
|
358
|
+
throw new AppError(HTTP_STATUS.GONE, 'Invite has already been used');
|
|
359
|
+
}
|
|
360
|
+
if (invite.expiresAt < new Date()) {
|
|
361
|
+
throw new AppError(HTTP_STATUS.GONE, 'Invite has expired');
|
|
362
|
+
}
|
|
363
|
+
// Verify email matches (optional - could allow any authenticated user)
|
|
364
|
+
if (invite.email.toLowerCase() !== req.user.email.toLowerCase()) {
|
|
365
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'This invite is for a different email address');
|
|
366
|
+
}
|
|
367
|
+
// Check if already a member
|
|
368
|
+
const existingMember = await db.teamMember.findUnique({
|
|
369
|
+
where: {
|
|
370
|
+
teamId_userId: {
|
|
371
|
+
teamId: invite.teamId,
|
|
372
|
+
userId: req.user.id,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
if (existingMember) {
|
|
377
|
+
throw new AppError(HTTP_STATUS.CONFLICT, 'You are already a member of this team');
|
|
378
|
+
}
|
|
379
|
+
// Create membership and mark invite as accepted
|
|
380
|
+
await db.$transaction([
|
|
381
|
+
db.teamMember.create({
|
|
382
|
+
data: {
|
|
383
|
+
teamId: invite.teamId,
|
|
384
|
+
userId: req.user.id,
|
|
385
|
+
role: invite.role,
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
db.teamInvite.update({
|
|
389
|
+
where: { id: invite.id },
|
|
390
|
+
data: { acceptedAt: new Date() },
|
|
391
|
+
}),
|
|
392
|
+
]);
|
|
393
|
+
// Send Slack notification for new team member
|
|
394
|
+
notifyTeamMemberJoined(invite.teamId, req.user.name, req.user.email, invite.team.name).catch(err => console.error('[Teams] Slack notification failed:', err));
|
|
395
|
+
res.json({
|
|
396
|
+
team: {
|
|
397
|
+
id: invite.team.id,
|
|
398
|
+
name: invite.team.name,
|
|
399
|
+
archivedAt: invite.team.archivedAt,
|
|
400
|
+
createdAt: invite.team.createdAt,
|
|
401
|
+
updatedAt: invite.team.updatedAt,
|
|
402
|
+
},
|
|
403
|
+
role: invite.role,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
next(error);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
// Cancel/delete invite (admin+)
|
|
411
|
+
teamsRouter.delete('/:teamId/invite/:inviteId', requireAuth, requireTeamRole('admin'), async (req, res, next) => {
|
|
412
|
+
try {
|
|
413
|
+
const { teamId, inviteId } = req.params;
|
|
414
|
+
const invite = await db.teamInvite.findUnique({
|
|
415
|
+
where: { id: inviteId },
|
|
416
|
+
});
|
|
417
|
+
if (!invite || invite.teamId !== teamId) {
|
|
418
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Invite not found');
|
|
419
|
+
}
|
|
420
|
+
await db.teamInvite.delete({
|
|
421
|
+
where: { id: inviteId },
|
|
422
|
+
});
|
|
423
|
+
res.status(HTTP_STATUS.NO_CONTENT).send();
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
next(error);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
// Remove member (admin+)
|
|
430
|
+
teamsRouter.delete('/:teamId/members/:userId', requireAuth, requireTeamRole('admin'), async (req, res, next) => {
|
|
431
|
+
try {
|
|
432
|
+
const { teamId, userId } = req.params;
|
|
433
|
+
// Can't remove yourself via this endpoint
|
|
434
|
+
if (userId === req.user.id) {
|
|
435
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Use leave endpoint to remove yourself');
|
|
436
|
+
}
|
|
437
|
+
const membership = await db.teamMember.findUnique({
|
|
438
|
+
where: {
|
|
439
|
+
teamId_userId: { teamId, userId },
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
if (!membership) {
|
|
443
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Member not found');
|
|
444
|
+
}
|
|
445
|
+
// Can't remove owner
|
|
446
|
+
if (membership.role === 'owner') {
|
|
447
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Cannot remove the team owner');
|
|
448
|
+
}
|
|
449
|
+
// Admins can't remove other admins (only owner can)
|
|
450
|
+
if (membership.role === 'admin' && req.teamMember?.role !== 'owner') {
|
|
451
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Only the owner can remove admins');
|
|
452
|
+
}
|
|
453
|
+
await db.teamMember.delete({
|
|
454
|
+
where: { id: membership.id },
|
|
455
|
+
});
|
|
456
|
+
res.status(HTTP_STATUS.NO_CONTENT).send();
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
next(error);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// Update member role (owner only)
|
|
463
|
+
teamsRouter.patch('/:teamId/members/:userId', requireAuth, requireTeamRole('owner'), async (req, res, next) => {
|
|
464
|
+
try {
|
|
465
|
+
const { teamId, userId } = req.params;
|
|
466
|
+
const { role } = updateMemberRoleSchema.parse(req.body);
|
|
467
|
+
// Can't change your own role
|
|
468
|
+
if (userId === req.user.id) {
|
|
469
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Cannot change your own role');
|
|
470
|
+
}
|
|
471
|
+
const membership = await db.teamMember.findUnique({
|
|
472
|
+
where: {
|
|
473
|
+
teamId_userId: { teamId, userId },
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
if (!membership) {
|
|
477
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Member not found');
|
|
478
|
+
}
|
|
479
|
+
// Can't make someone else an owner (would need ownership transfer)
|
|
480
|
+
if (role === 'owner') {
|
|
481
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Cannot assign owner role. Use ownership transfer instead.');
|
|
482
|
+
}
|
|
483
|
+
const updatedMembership = await db.teamMember.update({
|
|
484
|
+
where: { id: membership.id },
|
|
485
|
+
data: { role },
|
|
486
|
+
include: {
|
|
487
|
+
user: {
|
|
488
|
+
select: {
|
|
489
|
+
id: true,
|
|
490
|
+
email: true,
|
|
491
|
+
name: true,
|
|
492
|
+
avatarUrl: true,
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
res.json({
|
|
498
|
+
member: {
|
|
499
|
+
id: updatedMembership.id,
|
|
500
|
+
teamId: updatedMembership.teamId,
|
|
501
|
+
userId: updatedMembership.userId,
|
|
502
|
+
role: updatedMembership.role,
|
|
503
|
+
createdAt: updatedMembership.createdAt,
|
|
504
|
+
user: updatedMembership.user,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
next(error);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
// Leave team
|
|
513
|
+
teamsRouter.post('/:teamId/leave', requireAuth, requireTeamRole('viewer'), async (req, res, next) => {
|
|
514
|
+
try {
|
|
515
|
+
const { teamId } = req.params;
|
|
516
|
+
// Owner can't leave without transferring ownership
|
|
517
|
+
if (req.teamMember?.role === 'owner') {
|
|
518
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Owner cannot leave. Transfer ownership first or archive the team.');
|
|
519
|
+
}
|
|
520
|
+
await db.teamMember.delete({
|
|
521
|
+
where: {
|
|
522
|
+
teamId_userId: {
|
|
523
|
+
teamId,
|
|
524
|
+
userId: req.user.id,
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
res.status(HTTP_STATUS.NO_CONTENT).send();
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
next(error);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
// Get team threads
|
|
535
|
+
teamsRouter.get('/:teamId/threads', requireAuth, requireTeamRole('viewer'), async (req, res, next) => {
|
|
536
|
+
try {
|
|
537
|
+
const { teamId } = req.params;
|
|
538
|
+
const threads = await db.thread.findMany({
|
|
539
|
+
where: { teamId },
|
|
540
|
+
select: {
|
|
541
|
+
id: true,
|
|
542
|
+
title: true,
|
|
543
|
+
agentId: true,
|
|
544
|
+
parentThreadId: true,
|
|
545
|
+
createdAt: true,
|
|
546
|
+
updatedAt: true,
|
|
547
|
+
_count: {
|
|
548
|
+
select: { messages: true },
|
|
549
|
+
},
|
|
550
|
+
messages: {
|
|
551
|
+
orderBy: { createdAt: 'desc' },
|
|
552
|
+
take: 1,
|
|
553
|
+
select: { content: true },
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
orderBy: { updatedAt: 'desc' },
|
|
557
|
+
});
|
|
558
|
+
const threadSummaries = threads.map((thread) => {
|
|
559
|
+
const agent = getAgentById(thread.agentId);
|
|
560
|
+
return {
|
|
561
|
+
id: thread.id,
|
|
562
|
+
title: thread.title,
|
|
563
|
+
agentId: thread.agentId,
|
|
564
|
+
agentName: agent?.name,
|
|
565
|
+
parentThreadId: thread.parentThreadId || undefined,
|
|
566
|
+
createdAt: thread.createdAt,
|
|
567
|
+
updatedAt: thread.updatedAt,
|
|
568
|
+
messageCount: thread._count.messages,
|
|
569
|
+
lastMessagePreview: thread.messages[0]?.content.slice(0, 100),
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
res.json({ threads: threadSummaries });
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
next(error);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
// Get team stats
|
|
579
|
+
teamsRouter.get('/:teamId/stats', requireAuth, requireTeamRole('viewer'), async (req, res, next) => {
|
|
580
|
+
try {
|
|
581
|
+
const { teamId } = req.params;
|
|
582
|
+
// Get the start of the current month
|
|
583
|
+
const now = new Date();
|
|
584
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
585
|
+
// Run all queries in parallel for efficiency
|
|
586
|
+
const [totalThreads, totalMessages, messagesThisMonth, threadsThisMonth, memberCount, recentThreads, recentMembers,] = await Promise.all([
|
|
587
|
+
// Total threads
|
|
588
|
+
db.thread.count({
|
|
589
|
+
where: { teamId },
|
|
590
|
+
}),
|
|
591
|
+
// Total messages (in team threads)
|
|
592
|
+
db.message.count({
|
|
593
|
+
where: {
|
|
594
|
+
thread: { teamId },
|
|
595
|
+
},
|
|
596
|
+
}),
|
|
597
|
+
// Messages this month
|
|
598
|
+
db.message.count({
|
|
599
|
+
where: {
|
|
600
|
+
thread: { teamId },
|
|
601
|
+
createdAt: { gte: startOfMonth },
|
|
602
|
+
},
|
|
603
|
+
}),
|
|
604
|
+
// Threads this month
|
|
605
|
+
db.thread.count({
|
|
606
|
+
where: {
|
|
607
|
+
teamId,
|
|
608
|
+
createdAt: { gte: startOfMonth },
|
|
609
|
+
},
|
|
610
|
+
}),
|
|
611
|
+
// Member count
|
|
612
|
+
db.teamMember.count({
|
|
613
|
+
where: { teamId },
|
|
614
|
+
}),
|
|
615
|
+
// Recent threads (for activity)
|
|
616
|
+
db.thread.findMany({
|
|
617
|
+
where: { teamId },
|
|
618
|
+
orderBy: { createdAt: 'desc' },
|
|
619
|
+
take: 10,
|
|
620
|
+
select: {
|
|
621
|
+
id: true,
|
|
622
|
+
title: true,
|
|
623
|
+
createdAt: true,
|
|
624
|
+
userId: true,
|
|
625
|
+
user: {
|
|
626
|
+
select: {
|
|
627
|
+
id: true,
|
|
628
|
+
name: true,
|
|
629
|
+
email: true,
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
}),
|
|
634
|
+
// Recent member joins (for activity)
|
|
635
|
+
db.teamMember.findMany({
|
|
636
|
+
where: { teamId },
|
|
637
|
+
orderBy: { createdAt: 'desc' },
|
|
638
|
+
take: 10,
|
|
639
|
+
select: {
|
|
640
|
+
id: true,
|
|
641
|
+
createdAt: true,
|
|
642
|
+
user: {
|
|
643
|
+
select: {
|
|
644
|
+
id: true,
|
|
645
|
+
name: true,
|
|
646
|
+
email: true,
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
}),
|
|
651
|
+
]);
|
|
652
|
+
const stats = {
|
|
653
|
+
totalThreads,
|
|
654
|
+
totalMessages,
|
|
655
|
+
messagesThisMonth,
|
|
656
|
+
threadsThisMonth,
|
|
657
|
+
memberCount,
|
|
658
|
+
};
|
|
659
|
+
// Combine and sort activity items (filter out any with null users)
|
|
660
|
+
const activityItems = [
|
|
661
|
+
...recentThreads
|
|
662
|
+
.filter((thread) => thread.user !== null)
|
|
663
|
+
.map((thread) => ({
|
|
664
|
+
type: 'thread_created',
|
|
665
|
+
timestamp: thread.createdAt,
|
|
666
|
+
user: {
|
|
667
|
+
id: thread.user.id,
|
|
668
|
+
name: thread.user.name,
|
|
669
|
+
email: thread.user.email,
|
|
670
|
+
},
|
|
671
|
+
details: thread.title || 'Untitled thread',
|
|
672
|
+
})),
|
|
673
|
+
...recentMembers.map((member) => ({
|
|
674
|
+
type: 'member_joined',
|
|
675
|
+
timestamp: member.createdAt,
|
|
676
|
+
user: {
|
|
677
|
+
id: member.user.id,
|
|
678
|
+
name: member.user.name,
|
|
679
|
+
email: member.user.email,
|
|
680
|
+
},
|
|
681
|
+
})),
|
|
682
|
+
];
|
|
683
|
+
// Sort by timestamp descending and take the 10 most recent
|
|
684
|
+
const recentActivity = activityItems
|
|
685
|
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
686
|
+
.slice(0, 10);
|
|
687
|
+
res.json({ stats, recentActivity });
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
next(error);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
//# sourceMappingURL=teams.js.map
|