@equilateral_ai/mindmeld 3.3.1 → 3.4.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 +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-start.js +635 -41
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +12 -8
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +8 -5
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company Users List Handler
|
|
3
|
+
* Lists all users (entitlements) for a company
|
|
4
|
+
*
|
|
5
|
+
* GET /api/company/users?company_id=xxx
|
|
6
|
+
* Auth: Cognito JWT required
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* List company users
|
|
13
|
+
* Requires member access to the company
|
|
14
|
+
*/
|
|
15
|
+
async function listCompanyUsers({ queryStringParameters: queryParams = {}, requestContext }) {
|
|
16
|
+
try {
|
|
17
|
+
const Request_ID = requestContext.requestId;
|
|
18
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
19
|
+
const companyId = queryParams.company_id;
|
|
20
|
+
|
|
21
|
+
if (!email) {
|
|
22
|
+
return createErrorResponse(401, 'Authentication required');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!companyId) {
|
|
26
|
+
return createErrorResponse(400, 'company_id query parameter is required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Verify caller has access to this company
|
|
30
|
+
const accessQuery = `
|
|
31
|
+
SELECT admin, member
|
|
32
|
+
FROM rapport.user_entitlements
|
|
33
|
+
WHERE email_address = $1 AND company_id = $2
|
|
34
|
+
`;
|
|
35
|
+
const accessCheck = await executeQuery(accessQuery, [email, companyId]);
|
|
36
|
+
|
|
37
|
+
if (accessCheck.rowCount === 0) {
|
|
38
|
+
return createErrorResponse(403, 'You do not have access to this company');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get all users for the company
|
|
42
|
+
const query = `
|
|
43
|
+
SELECT
|
|
44
|
+
ue.email_address,
|
|
45
|
+
ue.admin,
|
|
46
|
+
ue.member,
|
|
47
|
+
ue.client_id,
|
|
48
|
+
ue.company_id,
|
|
49
|
+
ue.billing_type,
|
|
50
|
+
u.first_name,
|
|
51
|
+
u.last_name,
|
|
52
|
+
u.user_status,
|
|
53
|
+
u.create_date,
|
|
54
|
+
u.last_updated
|
|
55
|
+
FROM rapport.user_entitlements ue
|
|
56
|
+
LEFT JOIN rapport.users u ON u.email_address = ue.email_address
|
|
57
|
+
WHERE ue.company_id = $1
|
|
58
|
+
ORDER BY ue.admin DESC, u.create_date ASC
|
|
59
|
+
`;
|
|
60
|
+
const result = await executeQuery(query, [companyId]);
|
|
61
|
+
|
|
62
|
+
const users = result.rows.map(row => ({
|
|
63
|
+
email: row.email_address,
|
|
64
|
+
display_name: [row.first_name, row.last_name].filter(Boolean).join(' ') || null,
|
|
65
|
+
role: row.admin ? 'admin' : 'member',
|
|
66
|
+
status: row.user_status || 'active',
|
|
67
|
+
billing_type: row.billing_type || 'self_paid',
|
|
68
|
+
created_at: row.create_date,
|
|
69
|
+
last_active: row.last_updated,
|
|
70
|
+
client_id: row.client_id,
|
|
71
|
+
company_id: row.company_id,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
return createSuccessResponse(
|
|
75
|
+
{ Records: users },
|
|
76
|
+
`Found ${users.length} user(s)`,
|
|
77
|
+
{
|
|
78
|
+
Total_Records: users.length,
|
|
79
|
+
Request_ID,
|
|
80
|
+
Timestamp: new Date().toISOString()
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Handler Error:', error);
|
|
86
|
+
return handleError(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
exports.handler = wrapHandler(listCompanyUsers);
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company Users Invite Handler
|
|
3
|
+
* Invites a user to a company by creating an entitlement and sending an invite email
|
|
4
|
+
* Supports two billing modes: self_pays (invitee pays) or admin_pays (license on admin's subscription)
|
|
5
|
+
*
|
|
6
|
+
* POST /api/company/users
|
|
7
|
+
* Body: { company_id, email, role, billing }
|
|
8
|
+
* Auth: Cognito JWT required
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, getTierConfig } = require('./helpers');
|
|
12
|
+
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
|
|
13
|
+
const Stripe = require('stripe');
|
|
14
|
+
|
|
15
|
+
const ses = new SESClient({ region: process.env.AWS_REGION || 'us-east-2' });
|
|
16
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Invite user to company
|
|
20
|
+
* Requires admin access to the company
|
|
21
|
+
*/
|
|
22
|
+
async function inviteCompanyUser({ body: requestBody = {}, requestContext }) {
|
|
23
|
+
try {
|
|
24
|
+
const Request_ID = requestContext.requestId;
|
|
25
|
+
const callerEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
26
|
+
const { company_id, email, role, billing = 'self_pays' } = requestBody;
|
|
27
|
+
|
|
28
|
+
if (!callerEmail) {
|
|
29
|
+
return createErrorResponse(401, 'Authentication required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!company_id || !email) {
|
|
33
|
+
return createErrorResponse(400, 'company_id and email are required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!['admin_pays', 'self_pays'].includes(billing)) {
|
|
37
|
+
return createErrorResponse(400, 'billing must be admin_pays or self_pays');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Verify caller is admin of this company
|
|
41
|
+
const adminQuery = `
|
|
42
|
+
SELECT ue.admin, ue.client_id, u.first_name, u.last_name
|
|
43
|
+
FROM rapport.user_entitlements ue
|
|
44
|
+
LEFT JOIN rapport.users u ON u.email_address = ue.email_address
|
|
45
|
+
WHERE ue.email_address = $1 AND ue.company_id = $2
|
|
46
|
+
`;
|
|
47
|
+
const adminCheck = await executeQuery(adminQuery, [callerEmail, company_id]);
|
|
48
|
+
|
|
49
|
+
if (adminCheck.rowCount === 0 || !adminCheck.rows[0].admin) {
|
|
50
|
+
return createErrorResponse(403, 'Admin access required to invite users');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const clientId = adminCheck.rows[0].client_id;
|
|
54
|
+
const inviterName = [adminCheck.rows[0].first_name, adminCheck.rows[0].last_name].filter(Boolean).join(' ') || callerEmail;
|
|
55
|
+
|
|
56
|
+
// Get client details for seat limits and Stripe info
|
|
57
|
+
const clientQuery = `
|
|
58
|
+
SELECT subscription_tier, seat_count, client_name, stripe_subscription_id, license_count
|
|
59
|
+
FROM rapport.clients WHERE client_id = $1
|
|
60
|
+
`;
|
|
61
|
+
const clientResult = await executeQuery(clientQuery, [clientId]);
|
|
62
|
+
const clientRecord = clientResult.rows[0];
|
|
63
|
+
const tierConfig = getTierConfig(clientRecord?.subscription_tier || 'free');
|
|
64
|
+
const maxCollaborators = tierConfig?.maxCollaborators;
|
|
65
|
+
const teamName = clientRecord?.client_name || 'their team';
|
|
66
|
+
const inviterTier = clientRecord?.subscription_tier || 'team';
|
|
67
|
+
|
|
68
|
+
// Check seat limits before allowing invite
|
|
69
|
+
if (maxCollaborators !== null) {
|
|
70
|
+
const countQuery = `
|
|
71
|
+
SELECT COUNT(*) as current_count
|
|
72
|
+
FROM rapport.user_entitlements WHERE company_id = $1
|
|
73
|
+
`;
|
|
74
|
+
const countResult = await executeQuery(countQuery, [company_id]);
|
|
75
|
+
const currentCount = parseInt(countResult.rows[0].current_count) || 0;
|
|
76
|
+
|
|
77
|
+
if (currentCount >= maxCollaborators) {
|
|
78
|
+
return createErrorResponse(403, `Seat limit reached (${currentCount}/${maxCollaborators}). Upgrade your plan to add more team members.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// For admin_pays, verify admin has an active Stripe subscription
|
|
83
|
+
if (billing === 'admin_pays' && !clientRecord?.stripe_subscription_id) {
|
|
84
|
+
return createErrorResponse(400, 'You need an active subscription to add licensed users. Subscribe first, then invite with "Add to my subscription".');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Ensure invited user exists in users table (create pending record if not)
|
|
88
|
+
await executeQuery(`
|
|
89
|
+
INSERT INTO rapport.users (email_address, user_status, active, create_date)
|
|
90
|
+
VALUES ($1, 'Pending', true, NOW())
|
|
91
|
+
ON CONFLICT (email_address) DO NOTHING
|
|
92
|
+
`, [email]);
|
|
93
|
+
|
|
94
|
+
// Create entitlement for invited user
|
|
95
|
+
const isAdmin = role === 'admin';
|
|
96
|
+
const billingType = billing === 'admin_pays' ? 'admin_paid' : 'self_paid';
|
|
97
|
+
const insertQuery = `
|
|
98
|
+
INSERT INTO rapport.user_entitlements (
|
|
99
|
+
email_address, client_id, company_id, admin, member, billing_type
|
|
100
|
+
) VALUES ($1, $2, $3, $4, true, $5)
|
|
101
|
+
ON CONFLICT (email_address, company_id) DO UPDATE SET
|
|
102
|
+
admin = EXCLUDED.admin,
|
|
103
|
+
member = true,
|
|
104
|
+
billing_type = EXCLUDED.billing_type
|
|
105
|
+
RETURNING email_address, admin, member, billing_type
|
|
106
|
+
`;
|
|
107
|
+
const result = await executeQuery(insertQuery, [email, clientId, company_id, isAdmin, billingType]);
|
|
108
|
+
|
|
109
|
+
// If admin_pays, update license count and Stripe subscription quantity
|
|
110
|
+
let stripeUpdated = false;
|
|
111
|
+
if (billing === 'admin_pays') {
|
|
112
|
+
// Increment license_count
|
|
113
|
+
const updateResult = await executeQuery(`
|
|
114
|
+
UPDATE rapport.clients
|
|
115
|
+
SET license_count = COALESCE(license_count, 1) + 1, last_updated = CURRENT_TIMESTAMP
|
|
116
|
+
WHERE client_id = $1
|
|
117
|
+
RETURNING license_count
|
|
118
|
+
`, [clientId]);
|
|
119
|
+
const newLicenseCount = updateResult.rows[0].license_count;
|
|
120
|
+
|
|
121
|
+
// Update Stripe subscription quantity
|
|
122
|
+
try {
|
|
123
|
+
const subscription = await stripe.subscriptions.retrieve(clientRecord.stripe_subscription_id);
|
|
124
|
+
const item = subscription.items.data[0];
|
|
125
|
+
if (item) {
|
|
126
|
+
await stripe.subscriptions.update(clientRecord.stripe_subscription_id, {
|
|
127
|
+
items: [{ id: item.id, quantity: newLicenseCount }],
|
|
128
|
+
proration_behavior: 'none'
|
|
129
|
+
});
|
|
130
|
+
stripeUpdated = true;
|
|
131
|
+
console.log(`[License] Updated Stripe quantity to ${newLicenseCount} for ${clientId}`);
|
|
132
|
+
}
|
|
133
|
+
} catch (stripeError) {
|
|
134
|
+
console.error('[License] Stripe update failed:', stripeError.message);
|
|
135
|
+
// Revert license_count since Stripe failed
|
|
136
|
+
await executeQuery(`
|
|
137
|
+
UPDATE rapport.clients
|
|
138
|
+
SET license_count = COALESCE(license_count, 2) - 1, last_updated = CURRENT_TIMESTAMP
|
|
139
|
+
WHERE client_id = $1
|
|
140
|
+
`, [clientId]);
|
|
141
|
+
return createErrorResponse(500, 'Failed to update subscription. Please try again or contact support.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Send invite email
|
|
146
|
+
const appUrl = process.env.APP_URL || 'https://app.mindmeld.dev';
|
|
147
|
+
// Admin-paid users don't need to go through checkout, so no ?tier= param
|
|
148
|
+
const signupUrl = billing === 'admin_pays'
|
|
149
|
+
? `${appUrl}/signup`
|
|
150
|
+
: `${appUrl}/signup?tier=${inviterTier}`;
|
|
151
|
+
let emailSent = false;
|
|
152
|
+
|
|
153
|
+
// Customize email message based on billing type
|
|
154
|
+
const billingNote = billing === 'admin_pays'
|
|
155
|
+
? 'Your subscription is covered — just sign up and start using MindMeld.'
|
|
156
|
+
: 'MindMeld brings intelligent standards and patterns directly into your AI coding sessions — helping your team write better code, faster.';
|
|
157
|
+
|
|
158
|
+
const emailParams = {
|
|
159
|
+
Source: process.env.EMAIL_FROM || 'noreply@mindmeld.dev',
|
|
160
|
+
Destination: {
|
|
161
|
+
ToAddresses: [email]
|
|
162
|
+
},
|
|
163
|
+
Message: {
|
|
164
|
+
Subject: {
|
|
165
|
+
Data: `${inviterName} invited you to join ${teamName} on MindMeld`,
|
|
166
|
+
Charset: 'UTF-8'
|
|
167
|
+
},
|
|
168
|
+
Body: {
|
|
169
|
+
Html: {
|
|
170
|
+
Data: `
|
|
171
|
+
<!DOCTYPE html>
|
|
172
|
+
<html>
|
|
173
|
+
<head>
|
|
174
|
+
<meta charset="utf-8">
|
|
175
|
+
<style>
|
|
176
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
177
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
178
|
+
.header { text-align: center; margin-bottom: 30px; }
|
|
179
|
+
.logo { font-size: 24px; font-weight: bold; color: #2563eb; }
|
|
180
|
+
.content { background: #f8fafc; border-radius: 8px; padding: 30px; margin-bottom: 20px; }
|
|
181
|
+
.button { display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500; }
|
|
182
|
+
.footer { text-align: center; color: #64748b; font-size: 14px; }
|
|
183
|
+
.role-badge { display: inline-block; background: #e0e7ff; color: #3730a3; padding: 4px 12px; border-radius: 20px; font-size: 14px; }
|
|
184
|
+
</style>
|
|
185
|
+
</head>
|
|
186
|
+
<body>
|
|
187
|
+
<div class="container">
|
|
188
|
+
<div class="header">
|
|
189
|
+
<div class="logo">MindMeld</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="content">
|
|
192
|
+
<p>Hi there,</p>
|
|
193
|
+
<p><strong>${inviterName}</strong> has invited you to join <strong>${teamName}</strong> on MindMeld.</p>
|
|
194
|
+
<p>Your role: <span class="role-badge">${isAdmin ? 'Admin' : 'Member'}</span></p>
|
|
195
|
+
<p>${billingNote}</p>
|
|
196
|
+
<p style="margin-top: 30px;">
|
|
197
|
+
<a href="${signupUrl}" class="button">Get Started</a>
|
|
198
|
+
</p>
|
|
199
|
+
<p style="margin-top: 20px; font-size: 14px; color: #64748b;">
|
|
200
|
+
Already have an account? <a href="${appUrl}" style="color: #2563eb;">Sign in</a> — your team access will be ready.
|
|
201
|
+
</p>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="footer">
|
|
204
|
+
<p>MindMeld - Intelligent standards for AI coding</p>
|
|
205
|
+
<p>Powered by Equilateral AI</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</body>
|
|
209
|
+
</html>
|
|
210
|
+
`,
|
|
211
|
+
Charset: 'UTF-8'
|
|
212
|
+
},
|
|
213
|
+
Text: {
|
|
214
|
+
Data: `${inviterName} has invited you to join ${teamName} on MindMeld.
|
|
215
|
+
|
|
216
|
+
Your role: ${isAdmin ? 'Admin' : 'Member'}
|
|
217
|
+
|
|
218
|
+
${billingNote}
|
|
219
|
+
|
|
220
|
+
Get started: ${signupUrl}
|
|
221
|
+
|
|
222
|
+
Already have an account? Sign in at ${appUrl} — your team access will be ready.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
MindMeld - Intelligent standards for AI coding
|
|
226
|
+
Powered by Equilateral AI
|
|
227
|
+
`,
|
|
228
|
+
Charset: 'UTF-8'
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await ses.send(new SendEmailCommand(emailParams));
|
|
236
|
+
emailSent = true;
|
|
237
|
+
} catch (sesError) {
|
|
238
|
+
console.error('SES Error sending invite:', sesError);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const responseRecords = result.rows.map(r => ({
|
|
242
|
+
...r,
|
|
243
|
+
email_sent: emailSent,
|
|
244
|
+
stripe_updated: stripeUpdated
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
const message = billing === 'admin_pays'
|
|
248
|
+
? `${email} added to your subscription (license ${stripeUpdated ? 'updated' : 'pending'})`
|
|
249
|
+
: emailSent ? `Invitation sent to ${email}` : `User ${email} added to team (email delivery failed — share the link manually)`;
|
|
250
|
+
|
|
251
|
+
return createSuccessResponse(
|
|
252
|
+
{ Records: responseRecords },
|
|
253
|
+
message,
|
|
254
|
+
{
|
|
255
|
+
Total_Records: result.rowCount,
|
|
256
|
+
Request_ID,
|
|
257
|
+
Timestamp: new Date().toISOString()
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('Handler Error:', error);
|
|
263
|
+
return handleError(error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
exports.handler = wrapHandler(inviteCompanyUser);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company Users Update Handler
|
|
3
|
+
* Updates a user's role within a company
|
|
4
|
+
*
|
|
5
|
+
* PUT /api/company/users
|
|
6
|
+
* Body: { company_id, email, role }
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Update user role in company
|
|
14
|
+
* Requires admin access to the company
|
|
15
|
+
*/
|
|
16
|
+
async function updateCompanyUser({ body: requestBody = {}, requestContext }) {
|
|
17
|
+
try {
|
|
18
|
+
const Request_ID = requestContext.requestId;
|
|
19
|
+
const callerEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
20
|
+
const { company_id, email, role } = requestBody;
|
|
21
|
+
|
|
22
|
+
if (!callerEmail) {
|
|
23
|
+
return createErrorResponse(401, 'Authentication required');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!company_id || !email || !role) {
|
|
27
|
+
return createErrorResponse(400, 'company_id, email, and role are required');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Verify caller is admin of this company
|
|
31
|
+
const adminQuery = `
|
|
32
|
+
SELECT admin FROM rapport.user_entitlements
|
|
33
|
+
WHERE email_address = $1 AND company_id = $2
|
|
34
|
+
`;
|
|
35
|
+
const adminCheck = await executeQuery(adminQuery, [callerEmail, company_id]);
|
|
36
|
+
|
|
37
|
+
if (adminCheck.rowCount === 0 || !adminCheck.rows[0].admin) {
|
|
38
|
+
return createErrorResponse(403, 'Admin access required to update user roles');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Prevent self-demotion from admin
|
|
42
|
+
if (email === callerEmail && role !== 'admin') {
|
|
43
|
+
return createErrorResponse(400, 'Cannot remove your own admin role');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Update the entitlement
|
|
47
|
+
const isAdmin = role === 'admin';
|
|
48
|
+
const updateQuery = `
|
|
49
|
+
UPDATE rapport.user_entitlements
|
|
50
|
+
SET admin = $1
|
|
51
|
+
WHERE email_address = $2 AND company_id = $3
|
|
52
|
+
RETURNING email_address, admin, member
|
|
53
|
+
`;
|
|
54
|
+
const result = await executeQuery(updateQuery, [isAdmin, email, company_id]);
|
|
55
|
+
|
|
56
|
+
if (result.rowCount === 0) {
|
|
57
|
+
return createErrorResponse(404, `User ${email} not found in this company`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return createSuccessResponse(
|
|
61
|
+
{ Records: result.rows },
|
|
62
|
+
`User ${email} role updated to ${role}`,
|
|
63
|
+
{
|
|
64
|
+
Total_Records: result.rowCount,
|
|
65
|
+
Request_ID,
|
|
66
|
+
Timestamp: new Date().toISOString()
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Handler Error:', error);
|
|
72
|
+
return handleError(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
exports.handler = wrapHandler(updateCompanyUser);
|
|
@@ -39,12 +39,12 @@ exports.handler = wrapHandler(async (event, context) => {
|
|
|
39
39
|
if (!isSelf) {
|
|
40
40
|
// Verify requesting user is admin in a shared company
|
|
41
41
|
const accessResult = await executeQuery(`
|
|
42
|
-
SELECT DISTINCT ue1.
|
|
43
|
-
FROM
|
|
44
|
-
JOIN
|
|
45
|
-
WHERE ue1.
|
|
46
|
-
AND ue2.
|
|
47
|
-
AND (ue1.
|
|
42
|
+
SELECT DISTINCT ue1.company_id
|
|
43
|
+
FROM rapport.user_entitlements ue1
|
|
44
|
+
JOIN rapport.user_entitlements ue2 ON ue1.company_id = ue2.company_id
|
|
45
|
+
WHERE ue1.email_address = $1
|
|
46
|
+
AND ue2.email_address = $2
|
|
47
|
+
AND (ue1.admin = true OR ue1.manager = true)
|
|
48
48
|
`, [requestingEmail, targetEmail]);
|
|
49
49
|
|
|
50
50
|
if (accessResult.rows.length === 0) {
|
|
@@ -55,12 +55,12 @@ exports.handler = wrapHandler(async (event, context) => {
|
|
|
55
55
|
// Get developer info
|
|
56
56
|
const developerResult = await executeQuery(`
|
|
57
57
|
SELECT
|
|
58
|
-
u.
|
|
59
|
-
u.
|
|
60
|
-
u.
|
|
61
|
-
u.
|
|
62
|
-
FROM
|
|
63
|
-
WHERE u.
|
|
58
|
+
u.email_address as email,
|
|
59
|
+
CONCAT(u.first_name, ' ', u.last_name) as display_name,
|
|
60
|
+
u.first_name,
|
|
61
|
+
u.last_name
|
|
62
|
+
FROM rapport.users u
|
|
63
|
+
WHERE u.email_address = $1
|
|
64
64
|
`, [targetEmail]);
|
|
65
65
|
|
|
66
66
|
if (developerResult.rows.length === 0) {
|
|
@@ -28,9 +28,9 @@ exports.handler = wrapHandler(async (event, context) => {
|
|
|
28
28
|
|
|
29
29
|
// Get user's company
|
|
30
30
|
const companyResult = await executeQuery(`
|
|
31
|
-
SELECT ue.
|
|
32
|
-
FROM
|
|
33
|
-
WHERE ue.
|
|
31
|
+
SELECT ue.company_id
|
|
32
|
+
FROM rapport.user_entitlements ue
|
|
33
|
+
WHERE ue.email_address = $1
|
|
34
34
|
LIMIT 1
|
|
35
35
|
`, [email]);
|
|
36
36
|
|
|
@@ -38,7 +38,7 @@ exports.handler = wrapHandler(async (event, context) => {
|
|
|
38
38
|
return createErrorResponse(403, 'User not associated with any company');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const companyId = companyResult.rows[0].
|
|
41
|
+
const companyId = companyResult.rows[0].company_id;
|
|
42
42
|
|
|
43
43
|
// Initialize analyzer
|
|
44
44
|
const analyzer = new CorrelationAnalyzer();
|
|
@@ -83,11 +83,11 @@ exports.handler = wrapHandler(async (event, context) => {
|
|
|
83
83
|
*/
|
|
84
84
|
async function checkIsAdmin(email, companyId) {
|
|
85
85
|
const result = await executeQuery(`
|
|
86
|
-
SELECT
|
|
87
|
-
FROM
|
|
88
|
-
WHERE
|
|
86
|
+
SELECT admin, manager
|
|
87
|
+
FROM rapport.user_entitlements
|
|
88
|
+
WHERE email_address = $1 AND company_id = $2
|
|
89
89
|
`, [email, companyId]);
|
|
90
90
|
|
|
91
91
|
if (result.rows.length === 0) return false;
|
|
92
|
-
return result.rows[0].
|
|
92
|
+
return result.rows[0].admin || result.rows[0].manager;
|
|
93
93
|
}
|
|
@@ -83,7 +83,7 @@ async function getDeveloperBreakdown(projectId, lookbackDays) {
|
|
|
83
83
|
const query = `
|
|
84
84
|
SELECT
|
|
85
85
|
sc.email_address,
|
|
86
|
-
u.
|
|
86
|
+
CONCAT(u.first_name, ' ', u.last_name) as display_name,
|
|
87
87
|
COUNT(*) as total_sessions,
|
|
88
88
|
COUNT(*) FILTER (WHERE sc.has_commits = true) as productive_sessions,
|
|
89
89
|
ROUND(
|
|
@@ -97,10 +97,10 @@ async function getDeveloperBreakdown(projectId, lookbackDays) {
|
|
|
97
97
|
ROUND(AVG(sc.session_duration_seconds) / 60, 0) as avg_session_minutes,
|
|
98
98
|
MAX(sc.session_started_at) as last_session
|
|
99
99
|
FROM rapport.session_correlations sc
|
|
100
|
-
JOIN
|
|
100
|
+
JOIN rapport.users u ON sc.email_address = u.email_address
|
|
101
101
|
WHERE sc.project_id = $1
|
|
102
102
|
AND sc.session_started_at > NOW() - $2 * INTERVAL '1 day'
|
|
103
|
-
GROUP BY sc.email_address, u.
|
|
103
|
+
GROUP BY sc.email_address, u.first_name, u.last_name
|
|
104
104
|
ORDER BY total_commits DESC NULLS LAST
|
|
105
105
|
`;
|
|
106
106
|
|
|
@@ -128,7 +128,7 @@ async function getRecentCorrelations(projectId, limit) {
|
|
|
128
128
|
SELECT
|
|
129
129
|
sc.session_id,
|
|
130
130
|
sc.email_address,
|
|
131
|
-
u.
|
|
131
|
+
CONCAT(u.first_name, ' ', u.last_name) as display_name,
|
|
132
132
|
sc.session_started_at,
|
|
133
133
|
sc.session_duration_seconds,
|
|
134
134
|
sc.has_commits,
|
|
@@ -138,7 +138,7 @@ async function getRecentCorrelations(projectId, limit) {
|
|
|
138
138
|
sc.correlation_type,
|
|
139
139
|
sc.correlation_score
|
|
140
140
|
FROM rapport.session_correlations sc
|
|
141
|
-
JOIN
|
|
141
|
+
JOIN rapport.users u ON sc.email_address = u.email_address
|
|
142
142
|
WHERE sc.project_id = $1
|
|
143
143
|
ORDER BY sc.session_started_at DESC
|
|
144
144
|
LIMIT $2
|