eggi-ai-db-schema-2 12.54.2
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/CHANGELOG.md +750 -0
- package/README.md +655 -0
- package/dist/config/database.d.ts +28 -0
- package/dist/config/database.d.ts.map +1 -0
- package/dist/config/database.js +72 -0
- package/dist/config/database.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +199 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/database-service.d.ts +689 -0
- package/dist/lib/database-service.d.ts.map +1 -0
- package/dist/lib/database-service.js +1362 -0
- package/dist/lib/database-service.js.map +1 -0
- package/dist/lib/db-types.d.ts +167 -0
- package/dist/lib/db-types.d.ts.map +1 -0
- package/dist/lib/db-types.js +28 -0
- package/dist/lib/db-types.js.map +1 -0
- package/dist/lib/db.d.ts +58 -0
- package/dist/lib/db.d.ts.map +1 -0
- package/dist/lib/db.js +292 -0
- package/dist/lib/db.js.map +1 -0
- package/dist/lib/index.d.ts +11 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/pg-client.d.ts +50 -0
- package/dist/lib/pg-client.d.ts.map +1 -0
- package/dist/lib/pg-client.js +106 -0
- package/dist/lib/pg-client.js.map +1 -0
- package/dist/lib/schema.d.ts +298 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +12 -0
- package/dist/lib/schema.js.map +1 -0
- package/dist/migration-manager.d.ts +49 -0
- package/dist/migration-manager.d.ts.map +1 -0
- package/dist/migration-manager.js +282 -0
- package/dist/migration-manager.js.map +1 -0
- package/dist/queries/minimal-connections.d.ts +31 -0
- package/dist/queries/minimal-connections.d.ts.map +1 -0
- package/dist/queries/minimal-connections.js +143 -0
- package/dist/queries/minimal-connections.js.map +1 -0
- package/dist/schema.ts +340 -0
- package/dist/seed.d.ts +8 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +40 -0
- package/dist/seed.js.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/types.d.ts +77 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/types.js +3 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/authenticated-user-operations.d.ts +110 -0
- package/dist/utils/authenticated-user-operations.d.ts.map +1 -0
- package/dist/utils/authenticated-user-operations.js +292 -0
- package/dist/utils/authenticated-user-operations.js.map +1 -0
- package/dist/utils/authentication-operations.d.ts +48 -0
- package/dist/utils/authentication-operations.d.ts.map +1 -0
- package/dist/utils/authentication-operations.js +172 -0
- package/dist/utils/authentication-operations.js.map +1 -0
- package/dist/utils/company-mapping-job-operations.d.ts +103 -0
- package/dist/utils/company-mapping-job-operations.d.ts.map +1 -0
- package/dist/utils/company-mapping-job-operations.js +413 -0
- package/dist/utils/company-mapping-job-operations.js.map +1 -0
- package/dist/utils/company-sheet-upload-operations.d.ts +53 -0
- package/dist/utils/company-sheet-upload-operations.d.ts.map +1 -0
- package/dist/utils/company-sheet-upload-operations.js +135 -0
- package/dist/utils/company-sheet-upload-operations.js.map +1 -0
- package/dist/utils/contact-operations.d.ts +70 -0
- package/dist/utils/contact-operations.d.ts.map +1 -0
- package/dist/utils/contact-operations.js +294 -0
- package/dist/utils/contact-operations.js.map +1 -0
- package/dist/utils/forager-linkedin-operations.d.ts +74 -0
- package/dist/utils/forager-linkedin-operations.d.ts.map +1 -0
- package/dist/utils/forager-linkedin-operations.js +778 -0
- package/dist/utils/forager-linkedin-operations.js.map +1 -0
- package/dist/utils/ghost-genius-linkedin-operations.d.ts +23 -0
- package/dist/utils/ghost-genius-linkedin-operations.d.ts.map +1 -0
- package/dist/utils/ghost-genius-linkedin-operations.js +282 -0
- package/dist/utils/ghost-genius-linkedin-operations.js.map +1 -0
- package/dist/utils/index.d.ts +29 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +77 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/introduction-request-operations.d.ts +159 -0
- package/dist/utils/introduction-request-operations.d.ts.map +1 -0
- package/dist/utils/introduction-request-operations.js +481 -0
- package/dist/utils/introduction-request-operations.js.map +1 -0
- package/dist/utils/invitation-operations.d.ts +141 -0
- package/dist/utils/invitation-operations.d.ts.map +1 -0
- package/dist/utils/invitation-operations.js +749 -0
- package/dist/utils/invitation-operations.js.map +1 -0
- package/dist/utils/linkedin-account-operations.d.ts +45 -0
- package/dist/utils/linkedin-account-operations.d.ts.map +1 -0
- package/dist/utils/linkedin-account-operations.js +279 -0
- package/dist/utils/linkedin-account-operations.js.map +1 -0
- package/dist/utils/linkedin-account-relationship-operations.d.ts +77 -0
- package/dist/utils/linkedin-account-relationship-operations.d.ts.map +1 -0
- package/dist/utils/linkedin-account-relationship-operations.js +274 -0
- package/dist/utils/linkedin-account-relationship-operations.js.map +1 -0
- package/dist/utils/linkedin-data-operations.d.ts +102 -0
- package/dist/utils/linkedin-data-operations.d.ts.map +1 -0
- package/dist/utils/linkedin-data-operations.js +613 -0
- package/dist/utils/linkedin-data-operations.js.map +1 -0
- package/dist/utils/linkedin-identifier-utils.d.ts +31 -0
- package/dist/utils/linkedin-identifier-utils.d.ts.map +1 -0
- package/dist/utils/linkedin-identifier-utils.js +63 -0
- package/dist/utils/linkedin-identifier-utils.js.map +1 -0
- package/dist/utils/linkedin-profile-cache.d.ts +131 -0
- package/dist/utils/linkedin-profile-cache.d.ts.map +1 -0
- package/dist/utils/linkedin-profile-cache.js +418 -0
- package/dist/utils/linkedin-profile-cache.js.map +1 -0
- package/dist/utils/llm-inference-job-operations.d.ts +116 -0
- package/dist/utils/llm-inference-job-operations.d.ts.map +1 -0
- package/dist/utils/llm-inference-job-operations.js +266 -0
- package/dist/utils/llm-inference-job-operations.js.map +1 -0
- package/dist/utils/mapping-job-operations.d.ts +272 -0
- package/dist/utils/mapping-job-operations.d.ts.map +1 -0
- package/dist/utils/mapping-job-operations.js +833 -0
- package/dist/utils/mapping-job-operations.js.map +1 -0
- package/dist/utils/mapping-operations.d.ts +80 -0
- package/dist/utils/mapping-operations.d.ts.map +1 -0
- package/dist/utils/mapping-operations.js +318 -0
- package/dist/utils/mapping-operations.js.map +1 -0
- package/dist/utils/on-demand-mapping-operations.d.ts +199 -0
- package/dist/utils/on-demand-mapping-operations.d.ts.map +1 -0
- package/dist/utils/on-demand-mapping-operations.js +728 -0
- package/dist/utils/on-demand-mapping-operations.js.map +1 -0
- package/dist/utils/onboarding-operations.d.ts +53 -0
- package/dist/utils/onboarding-operations.d.ts.map +1 -0
- package/dist/utils/onboarding-operations.js +223 -0
- package/dist/utils/onboarding-operations.js.map +1 -0
- package/dist/utils/organization-assignment-job-operations.d.ts +258 -0
- package/dist/utils/organization-assignment-job-operations.d.ts.map +1 -0
- package/dist/utils/organization-assignment-job-operations.js +881 -0
- package/dist/utils/organization-assignment-job-operations.js.map +1 -0
- package/dist/utils/organization-assignment-operations.d.ts +59 -0
- package/dist/utils/organization-assignment-operations.d.ts.map +1 -0
- package/dist/utils/organization-assignment-operations.js +130 -0
- package/dist/utils/organization-assignment-operations.js.map +1 -0
- package/dist/utils/organization-operations.d.ts +275 -0
- package/dist/utils/organization-operations.d.ts.map +1 -0
- package/dist/utils/organization-operations.js +993 -0
- package/dist/utils/organization-operations.js.map +1 -0
- package/dist/utils/organization-relationship-operations.d.ts +59 -0
- package/dist/utils/organization-relationship-operations.d.ts.map +1 -0
- package/dist/utils/organization-relationship-operations.js +240 -0
- package/dist/utils/organization-relationship-operations.js.map +1 -0
- package/dist/utils/quota-operations.d.ts +107 -0
- package/dist/utils/quota-operations.d.ts.map +1 -0
- package/dist/utils/quota-operations.js +692 -0
- package/dist/utils/quota-operations.js.map +1 -0
- package/dist/utils/recursive-mapping-job-operations.d.ts +42 -0
- package/dist/utils/recursive-mapping-job-operations.d.ts.map +1 -0
- package/dist/utils/recursive-mapping-job-operations.js +169 -0
- package/dist/utils/recursive-mapping-job-operations.js.map +1 -0
- package/dist/utils/relationship-operations.d.ts +130 -0
- package/dist/utils/relationship-operations.d.ts.map +1 -0
- package/dist/utils/relationship-operations.js +329 -0
- package/dist/utils/relationship-operations.js.map +1 -0
- package/dist/utils/sales-pipeline-operations.d.ts +143 -0
- package/dist/utils/sales-pipeline-operations.d.ts.map +1 -0
- package/dist/utils/sales-pipeline-operations.js +649 -0
- package/dist/utils/sales-pipeline-operations.js.map +1 -0
- package/dist/utils/skills-operations.d.ts +117 -0
- package/dist/utils/skills-operations.d.ts.map +1 -0
- package/dist/utils/skills-operations.js +487 -0
- package/dist/utils/skills-operations.js.map +1 -0
- package/dist/utils/subscription-operations.d.ts +123 -0
- package/dist/utils/subscription-operations.d.ts.map +1 -0
- package/dist/utils/subscription-operations.js +391 -0
- package/dist/utils/subscription-operations.js.map +1 -0
- package/dist/utils/unipile-account-operations.d.ts +96 -0
- package/dist/utils/unipile-account-operations.d.ts.map +1 -0
- package/dist/utils/unipile-account-operations.js +255 -0
- package/dist/utils/unipile-account-operations.js.map +1 -0
- package/dist/utils/user-industry-operations.d.ts +80 -0
- package/dist/utils/user-industry-operations.d.ts.map +1 -0
- package/dist/utils/user-industry-operations.js +237 -0
- package/dist/utils/user-industry-operations.js.map +1 -0
- package/dist/utils/user-operations.d.ts +87 -0
- package/dist/utils/user-operations.d.ts.map +1 -0
- package/dist/utils/user-operations.js +212 -0
- package/dist/utils/user-operations.js.map +1 -0
- package/package.json +98 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* =============================================================================
|
|
4
|
+
* INVITATION OPERATIONS - Database Operations for User Invitations
|
|
5
|
+
* =============================================================================
|
|
6
|
+
* Helper functions for managing user invitations to organizations
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.validateInvitationEligibility = validateInvitationEligibility;
|
|
13
|
+
exports.hasActiveInvitationByEmail = hasActiveInvitationByEmail;
|
|
14
|
+
exports.hasActiveInvitationByEmailForOrg = hasActiveInvitationByEmailForOrg;
|
|
15
|
+
exports.isEmailUsedByAuthenticatedUser = isEmailUsedByAuthenticatedUser;
|
|
16
|
+
exports.isEmailReservedByContactInfo = isEmailReservedByContactInfo;
|
|
17
|
+
exports.generateInvitationToken = generateInvitationToken;
|
|
18
|
+
exports.createInvitation = createInvitation;
|
|
19
|
+
exports.validateInvitationToken = validateInvitationToken;
|
|
20
|
+
exports.markInvitationAsUsed = markInvitationAsUsed;
|
|
21
|
+
exports.revokeInvitation = revokeInvitation;
|
|
22
|
+
exports.getInvitationById = getInvitationById;
|
|
23
|
+
exports.getInvitationByToken = getInvitationByToken;
|
|
24
|
+
exports.getPendingInvitationsByOrganization = getPendingInvitationsByOrganization;
|
|
25
|
+
exports.getAllInvitationsByOrganization = getAllInvitationsByOrganization;
|
|
26
|
+
exports.hasPendingInvitation = hasPendingInvitation;
|
|
27
|
+
exports.hasActiveInvitationForShadowUser = hasActiveInvitationForShadowUser;
|
|
28
|
+
exports.getInvitationsSentByUser = getInvitationsSentByUser;
|
|
29
|
+
exports.getPendingInvitationsSentByUser = getPendingInvitationsSentByUser;
|
|
30
|
+
exports.cleanupExpiredInvitations = cleanupExpiredInvitations;
|
|
31
|
+
const pg_client_1 = require("../lib/pg-client");
|
|
32
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// ELIGIBILITY CHECKS
|
|
35
|
+
// =============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Comprehensive validation for invitation eligibility
|
|
38
|
+
* Checks all three scenarios:
|
|
39
|
+
* a) No active invitation exists for this email
|
|
40
|
+
* b) Email is not already used by an authenticated user
|
|
41
|
+
* c) Email is not reserved by contact_info
|
|
42
|
+
*/
|
|
43
|
+
async function validateInvitationEligibility(db, email) {
|
|
44
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
45
|
+
// Check a) No active invitation exists for this email
|
|
46
|
+
const hasActiveInvitation = await hasActiveInvitationByEmail(db, normalizedEmail);
|
|
47
|
+
if (hasActiveInvitation) {
|
|
48
|
+
return { eligible: false, reason: "active_invitation" };
|
|
49
|
+
}
|
|
50
|
+
// Check b) Email is not already used by an authenticated user
|
|
51
|
+
const isEmailUsed = await isEmailUsedByAuthenticatedUser(db, normalizedEmail);
|
|
52
|
+
if (isEmailUsed) {
|
|
53
|
+
return { eligible: false, reason: "email_registered" };
|
|
54
|
+
}
|
|
55
|
+
// Check c) Email is not reserved by contact_info
|
|
56
|
+
const isEmailReserved = await isEmailReservedByContactInfo(db, normalizedEmail);
|
|
57
|
+
if (isEmailReserved) {
|
|
58
|
+
return { eligible: false, reason: "email_reserved" };
|
|
59
|
+
}
|
|
60
|
+
return { eligible: true };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns true if there is a still-active (pending) invitation for this email anywhere
|
|
64
|
+
* Active means: not used, not revoked, and not expired
|
|
65
|
+
*/
|
|
66
|
+
async function hasActiveInvitationByEmail(db, email) {
|
|
67
|
+
const normalized = email.toLowerCase().trim();
|
|
68
|
+
const sql = `
|
|
69
|
+
SELECT id FROM authentication.invitations
|
|
70
|
+
WHERE email = $1
|
|
71
|
+
AND used_at IS NULL
|
|
72
|
+
AND revoked_at IS NULL
|
|
73
|
+
AND expires_at > NOW()
|
|
74
|
+
LIMIT 1
|
|
75
|
+
`;
|
|
76
|
+
const row = await (0, pg_client_1.queryOne)(db, sql, [normalized]);
|
|
77
|
+
return !!row;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns true if there is a still-active (pending) invitation for this email in the given organization
|
|
81
|
+
*/
|
|
82
|
+
async function hasActiveInvitationByEmailForOrg(db, email, organizationId) {
|
|
83
|
+
const normalized = email.toLowerCase().trim();
|
|
84
|
+
const sql = `
|
|
85
|
+
SELECT id FROM authentication.invitations
|
|
86
|
+
WHERE email = $1
|
|
87
|
+
AND organization_id = $2
|
|
88
|
+
AND used_at IS NULL
|
|
89
|
+
AND revoked_at IS NULL
|
|
90
|
+
AND expires_at > NOW()
|
|
91
|
+
LIMIT 1
|
|
92
|
+
`;
|
|
93
|
+
const row = await (0, pg_client_1.queryOne)(db, sql, [normalized, organizationId]);
|
|
94
|
+
return !!row;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Email is already used by an authenticated user if there exists authentication.users → contact_infos value match (case-insensitive)
|
|
98
|
+
*/
|
|
99
|
+
async function isEmailUsedByAuthenticatedUser(db, email) {
|
|
100
|
+
const normalized = email.toLowerCase().trim();
|
|
101
|
+
const sql = `
|
|
102
|
+
SELECT 1 FROM authentication.users au
|
|
103
|
+
JOIN public.contact_infos ci ON ci.id = au.contact_info_id
|
|
104
|
+
WHERE LOWER(ci.value) = $1
|
|
105
|
+
LIMIT 1
|
|
106
|
+
`;
|
|
107
|
+
const row = await (0, pg_client_1.queryOne)(db, sql, [normalized]);
|
|
108
|
+
return !!row;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Returns true if any contact_info indicates the email is reserved/owned and should not be invited again.
|
|
112
|
+
* Criteria: ci.type = 'EMAIL' OR ci.source IN ('MANUAL', 'INVITATION') for the same email value.
|
|
113
|
+
*/
|
|
114
|
+
async function isEmailReservedByContactInfo(db, email) {
|
|
115
|
+
const normalized = email.toLowerCase().trim();
|
|
116
|
+
const sql = `
|
|
117
|
+
SELECT 1 FROM public.contact_infos ci
|
|
118
|
+
LEFT JOIN authentication.users au ON au.contact_info_id = ci.id
|
|
119
|
+
WHERE LOWER(ci.value) = $1
|
|
120
|
+
AND (
|
|
121
|
+
(ci.type = 'EMAIL' AND au.id IS NOT NULL)
|
|
122
|
+
OR (ci.source IN ('MANUAL', 'INVITATION') AND au.id IS NOT NULL)
|
|
123
|
+
)
|
|
124
|
+
LIMIT 1
|
|
125
|
+
`;
|
|
126
|
+
const row = await (0, pg_client_1.queryOne)(db, sql, [normalized]);
|
|
127
|
+
return !!row;
|
|
128
|
+
}
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// TOKEN GENERATION
|
|
131
|
+
// =============================================================================
|
|
132
|
+
/**
|
|
133
|
+
* Generate a cryptographically secure invitation token
|
|
134
|
+
* @returns 32-character hex string
|
|
135
|
+
*/
|
|
136
|
+
function generateInvitationToken() {
|
|
137
|
+
return crypto_1.default.randomBytes(16).toString("hex");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Helper function to fetch shadow user details with profile picture
|
|
141
|
+
*/
|
|
142
|
+
async function fetchShadowUserWithProfilePicture(db, shadowUserId) {
|
|
143
|
+
const userSql = `
|
|
144
|
+
SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1
|
|
145
|
+
`;
|
|
146
|
+
const shadowUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [shadowUserId]);
|
|
147
|
+
if (!shadowUserRecord) {
|
|
148
|
+
throw new Error(`Shadow user with ID ${shadowUserId} not found`);
|
|
149
|
+
}
|
|
150
|
+
// Get LinkedIn profile picture if available (prefer CDN URL over regular LinkedIn URL)
|
|
151
|
+
const linkedinSql = `
|
|
152
|
+
SELECT
|
|
153
|
+
profile_picture_url,
|
|
154
|
+
profile_image_cloudfront_url
|
|
155
|
+
FROM linkedin.accounts
|
|
156
|
+
WHERE user_id = $1
|
|
157
|
+
LIMIT 1
|
|
158
|
+
`;
|
|
159
|
+
const linkedinAccount = await (0, pg_client_1.queryOne)(db, linkedinSql, [shadowUserId]);
|
|
160
|
+
// Prefer CDN URL over regular LinkedIn URL
|
|
161
|
+
const profilePictureUrl = linkedinAccount?.profile_image_cloudfront_url || linkedinAccount?.profile_picture_url || null;
|
|
162
|
+
return {
|
|
163
|
+
id: shadowUserRecord.id,
|
|
164
|
+
givenName: shadowUserRecord.given_name,
|
|
165
|
+
familyName: shadowUserRecord.family_name,
|
|
166
|
+
profilePictureUrl: profilePictureUrl,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// =============================================================================
|
|
170
|
+
// CREATE INVITATION
|
|
171
|
+
// =============================================================================
|
|
172
|
+
/**
|
|
173
|
+
* Create a new invitation
|
|
174
|
+
*/
|
|
175
|
+
async function createInvitation(db, input) {
|
|
176
|
+
const token = generateInvitationToken();
|
|
177
|
+
const expiresInDays = input.expiresInDays ?? 7;
|
|
178
|
+
const normalizedEmail = input.email.toLowerCase().trim();
|
|
179
|
+
// Comprehensive validation: Check all three scenarios
|
|
180
|
+
const eligibilityCheck = await validateInvitationEligibility(db, normalizedEmail);
|
|
181
|
+
if (!eligibilityCheck.eligible) {
|
|
182
|
+
switch (eligibilityCheck.reason) {
|
|
183
|
+
case "active_invitation":
|
|
184
|
+
throw new Error("DUPLICATE_ACTIVE_INVITATION");
|
|
185
|
+
case "email_registered":
|
|
186
|
+
throw new Error("EMAIL_ALREADY_REGISTERED");
|
|
187
|
+
case "email_reserved":
|
|
188
|
+
throw new Error("EMAIL_ALREADY_RESERVED");
|
|
189
|
+
default:
|
|
190
|
+
throw new Error("INVITATION_NOT_ELIGIBLE");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Handle contact info creation/retrieval for invitation
|
|
194
|
+
const contactInfoId = await ensureContactInfoForInvitation(db, normalizedEmail, input.shadowUserId);
|
|
195
|
+
const sql = `
|
|
196
|
+
INSERT INTO authentication.invitations (
|
|
197
|
+
email, token, invitation_message, invited_user_permissions_role,
|
|
198
|
+
invited_by_user_id, shadow_user_id, organization_id, contact_info_id,
|
|
199
|
+
expires_at, created_at
|
|
200
|
+
)
|
|
201
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW() + ($9 * INTERVAL '1 day'), NOW())
|
|
202
|
+
RETURNING
|
|
203
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
204
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
205
|
+
created_at, expires_at, used_at, revoked_at
|
|
206
|
+
`;
|
|
207
|
+
const invitation = await (0, pg_client_1.queryOne)(db, sql, [
|
|
208
|
+
normalizedEmail,
|
|
209
|
+
token,
|
|
210
|
+
input.invitationMessage || null,
|
|
211
|
+
input.invitedUserPermissionsRole || "REGULAR",
|
|
212
|
+
input.invitedByUserId,
|
|
213
|
+
input.shadowUserId,
|
|
214
|
+
input.organizationId,
|
|
215
|
+
contactInfoId,
|
|
216
|
+
expiresInDays
|
|
217
|
+
]);
|
|
218
|
+
if (!invitation) {
|
|
219
|
+
throw new Error("Failed to create invitation");
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
id: invitation.id,
|
|
223
|
+
email: invitation.email,
|
|
224
|
+
token: invitation.token,
|
|
225
|
+
invitationMessage: invitation.invitation_message,
|
|
226
|
+
invitedUserPermissionsRole: invitation.invited_user_permissions_role,
|
|
227
|
+
invitedByUserId: invitation.invited_by_user_id,
|
|
228
|
+
shadowUserId: invitation.shadow_user_id,
|
|
229
|
+
organizationId: invitation.organization_id,
|
|
230
|
+
createdAt: invitation.created_at,
|
|
231
|
+
expiresAt: invitation.expires_at,
|
|
232
|
+
usedAt: invitation.used_at,
|
|
233
|
+
revokedAt: invitation.revoked_at,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// VALIDATE TOKEN
|
|
238
|
+
// =============================================================================
|
|
239
|
+
/**
|
|
240
|
+
* Validate an invitation token and return invitation details
|
|
241
|
+
*/
|
|
242
|
+
async function validateInvitationToken(db, token) {
|
|
243
|
+
// First get the invitation
|
|
244
|
+
const invitationSql = `
|
|
245
|
+
SELECT
|
|
246
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
247
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
248
|
+
created_at, expires_at, used_at, revoked_at
|
|
249
|
+
FROM authentication.invitations
|
|
250
|
+
WHERE token = $1
|
|
251
|
+
LIMIT 1
|
|
252
|
+
`;
|
|
253
|
+
const invitationRecord = await (0, pg_client_1.queryOne)(db, invitationSql, [token]);
|
|
254
|
+
if (!invitationRecord) {
|
|
255
|
+
return { valid: false, reason: "not_found" };
|
|
256
|
+
}
|
|
257
|
+
// Check if already used
|
|
258
|
+
if (invitationRecord.used_at) {
|
|
259
|
+
return { valid: false, reason: "already_used" };
|
|
260
|
+
}
|
|
261
|
+
// Check if revoked
|
|
262
|
+
if (invitationRecord.revoked_at) {
|
|
263
|
+
return { valid: false, reason: "revoked" };
|
|
264
|
+
}
|
|
265
|
+
// Check if expired
|
|
266
|
+
if (new Date() > new Date(invitationRecord.expires_at)) {
|
|
267
|
+
return { valid: false, reason: "expired" };
|
|
268
|
+
}
|
|
269
|
+
// Get inviter user details with LinkedIn profile picture
|
|
270
|
+
const invitedByUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.invited_by_user_id);
|
|
271
|
+
// Get shadow user details with LinkedIn profile picture
|
|
272
|
+
const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
|
|
273
|
+
const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
|
|
274
|
+
const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
|
|
275
|
+
if (!organizationRecord) {
|
|
276
|
+
throw new Error(`Organization ${invitationRecord.organization_id} not found`);
|
|
277
|
+
}
|
|
278
|
+
const invitation = {
|
|
279
|
+
id: invitationRecord.id,
|
|
280
|
+
email: invitationRecord.email,
|
|
281
|
+
token: invitationRecord.token,
|
|
282
|
+
invitationMessage: invitationRecord.invitation_message,
|
|
283
|
+
invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
|
|
284
|
+
invitedByUserId: invitationRecord.invited_by_user_id,
|
|
285
|
+
shadowUserId: invitationRecord.shadow_user_id,
|
|
286
|
+
organizationId: invitationRecord.organization_id,
|
|
287
|
+
createdAt: invitationRecord.created_at,
|
|
288
|
+
expiresAt: invitationRecord.expires_at,
|
|
289
|
+
usedAt: invitationRecord.used_at,
|
|
290
|
+
revokedAt: invitationRecord.revoked_at,
|
|
291
|
+
invitedByUser: invitedByUserRecord,
|
|
292
|
+
shadowUser: shadowUserRecord,
|
|
293
|
+
organization: organizationRecord,
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
valid: true,
|
|
297
|
+
invitation,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// =============================================================================
|
|
301
|
+
// MARK AS USED
|
|
302
|
+
// =============================================================================
|
|
303
|
+
/**
|
|
304
|
+
* Mark an invitation as used
|
|
305
|
+
*/
|
|
306
|
+
async function markInvitationAsUsed(db, token) {
|
|
307
|
+
const sql = `
|
|
308
|
+
UPDATE authentication.invitations
|
|
309
|
+
SET used_at = NOW()
|
|
310
|
+
WHERE token = $1
|
|
311
|
+
RETURNING
|
|
312
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
313
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
314
|
+
created_at, expires_at, used_at, revoked_at
|
|
315
|
+
`;
|
|
316
|
+
const invitation = await (0, pg_client_1.queryOne)(db, sql, [token]);
|
|
317
|
+
if (!invitation)
|
|
318
|
+
return null;
|
|
319
|
+
return {
|
|
320
|
+
id: invitation.id,
|
|
321
|
+
email: invitation.email,
|
|
322
|
+
token: invitation.token,
|
|
323
|
+
invitationMessage: invitation.invitation_message,
|
|
324
|
+
invitedUserPermissionsRole: invitation.invited_user_permissions_role,
|
|
325
|
+
invitedByUserId: invitation.invited_by_user_id,
|
|
326
|
+
shadowUserId: invitation.shadow_user_id,
|
|
327
|
+
organizationId: invitation.organization_id,
|
|
328
|
+
createdAt: invitation.created_at,
|
|
329
|
+
expiresAt: invitation.expires_at,
|
|
330
|
+
usedAt: invitation.used_at,
|
|
331
|
+
revokedAt: invitation.revoked_at,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// =============================================================================
|
|
335
|
+
// REVOKE INVITATION
|
|
336
|
+
// =============================================================================
|
|
337
|
+
/**
|
|
338
|
+
* Revoke an invitation
|
|
339
|
+
*/
|
|
340
|
+
async function revokeInvitation(db, invitationId) {
|
|
341
|
+
const sql = `
|
|
342
|
+
UPDATE authentication.invitations
|
|
343
|
+
SET revoked_at = NOW()
|
|
344
|
+
WHERE id = $1
|
|
345
|
+
AND used_at IS NULL
|
|
346
|
+
AND revoked_at IS NULL
|
|
347
|
+
RETURNING
|
|
348
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
349
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
350
|
+
created_at, expires_at, used_at, revoked_at
|
|
351
|
+
`;
|
|
352
|
+
const invitation = await (0, pg_client_1.queryOne)(db, sql, [invitationId]);
|
|
353
|
+
if (!invitation)
|
|
354
|
+
return null;
|
|
355
|
+
return {
|
|
356
|
+
id: invitation.id,
|
|
357
|
+
email: invitation.email,
|
|
358
|
+
token: invitation.token,
|
|
359
|
+
invitationMessage: invitation.invitation_message,
|
|
360
|
+
invitedUserPermissionsRole: invitation.invited_user_permissions_role,
|
|
361
|
+
invitedByUserId: invitation.invited_by_user_id,
|
|
362
|
+
shadowUserId: invitation.shadow_user_id,
|
|
363
|
+
organizationId: invitation.organization_id,
|
|
364
|
+
createdAt: invitation.created_at,
|
|
365
|
+
expiresAt: invitation.expires_at,
|
|
366
|
+
usedAt: invitation.used_at,
|
|
367
|
+
revokedAt: invitation.revoked_at,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// =============================================================================
|
|
371
|
+
// GET INVITATION
|
|
372
|
+
// =============================================================================
|
|
373
|
+
/**
|
|
374
|
+
* Get invitation by ID
|
|
375
|
+
*/
|
|
376
|
+
async function getInvitationById(db, invitationId) {
|
|
377
|
+
const invitationSql = `
|
|
378
|
+
SELECT
|
|
379
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
380
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
381
|
+
created_at, expires_at, used_at, revoked_at
|
|
382
|
+
FROM authentication.invitations
|
|
383
|
+
WHERE id = $1
|
|
384
|
+
LIMIT 1
|
|
385
|
+
`;
|
|
386
|
+
const invitationRecord = await (0, pg_client_1.queryOne)(db, invitationSql, [invitationId]);
|
|
387
|
+
if (!invitationRecord) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
|
|
391
|
+
const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
|
|
392
|
+
const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
|
|
393
|
+
const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
|
|
394
|
+
const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
|
|
395
|
+
if (!invitedByUserRecord || !organizationRecord) {
|
|
396
|
+
throw new Error("Missing related records for invitation");
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
id: invitationRecord.id,
|
|
400
|
+
email: invitationRecord.email,
|
|
401
|
+
token: invitationRecord.token,
|
|
402
|
+
invitationMessage: invitationRecord.invitation_message,
|
|
403
|
+
invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
|
|
404
|
+
invitedByUserId: invitationRecord.invited_by_user_id,
|
|
405
|
+
shadowUserId: invitationRecord.shadow_user_id,
|
|
406
|
+
organizationId: invitationRecord.organization_id,
|
|
407
|
+
createdAt: invitationRecord.created_at,
|
|
408
|
+
expiresAt: invitationRecord.expires_at,
|
|
409
|
+
usedAt: invitationRecord.used_at,
|
|
410
|
+
revokedAt: invitationRecord.revoked_at,
|
|
411
|
+
invitedByUser: {
|
|
412
|
+
id: invitedByUserRecord.id,
|
|
413
|
+
givenName: invitedByUserRecord.given_name,
|
|
414
|
+
familyName: invitedByUserRecord.family_name,
|
|
415
|
+
},
|
|
416
|
+
shadowUser: shadowUserRecord,
|
|
417
|
+
organization: organizationRecord,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get invitation by token
|
|
422
|
+
*/
|
|
423
|
+
async function getInvitationByToken(db, token) {
|
|
424
|
+
const invitationSql = `
|
|
425
|
+
SELECT
|
|
426
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
427
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
428
|
+
created_at, expires_at, used_at, revoked_at
|
|
429
|
+
FROM authentication.invitations
|
|
430
|
+
WHERE token = $1
|
|
431
|
+
LIMIT 1
|
|
432
|
+
`;
|
|
433
|
+
const invitationRecord = await (0, pg_client_1.queryOne)(db, invitationSql, [token]);
|
|
434
|
+
if (!invitationRecord) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
|
|
438
|
+
const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
|
|
439
|
+
// Get shadow user details with LinkedIn profile picture
|
|
440
|
+
const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
|
|
441
|
+
const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
|
|
442
|
+
const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
|
|
443
|
+
if (!invitedByUserRecord || !organizationRecord) {
|
|
444
|
+
throw new Error("Missing related records for invitation");
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
id: invitationRecord.id,
|
|
448
|
+
email: invitationRecord.email,
|
|
449
|
+
token: invitationRecord.token,
|
|
450
|
+
invitationMessage: invitationRecord.invitation_message,
|
|
451
|
+
invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
|
|
452
|
+
invitedByUserId: invitationRecord.invited_by_user_id,
|
|
453
|
+
shadowUserId: invitationRecord.shadow_user_id,
|
|
454
|
+
organizationId: invitationRecord.organization_id,
|
|
455
|
+
createdAt: invitationRecord.created_at,
|
|
456
|
+
expiresAt: invitationRecord.expires_at,
|
|
457
|
+
usedAt: invitationRecord.used_at,
|
|
458
|
+
revokedAt: invitationRecord.revoked_at,
|
|
459
|
+
invitedByUser: {
|
|
460
|
+
id: invitedByUserRecord.id,
|
|
461
|
+
givenName: invitedByUserRecord.given_name,
|
|
462
|
+
familyName: invitedByUserRecord.family_name,
|
|
463
|
+
},
|
|
464
|
+
shadowUser: shadowUserRecord,
|
|
465
|
+
organization: organizationRecord,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
// =============================================================================
|
|
469
|
+
// LIST INVITATIONS
|
|
470
|
+
// =============================================================================
|
|
471
|
+
/**
|
|
472
|
+
* Get all pending invitations for an organization
|
|
473
|
+
*/
|
|
474
|
+
async function getPendingInvitationsByOrganization(db, organizationId) {
|
|
475
|
+
const invitationsSql = `
|
|
476
|
+
SELECT
|
|
477
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
478
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
479
|
+
created_at, expires_at, used_at, revoked_at
|
|
480
|
+
FROM authentication.invitations
|
|
481
|
+
WHERE organization_id = $1
|
|
482
|
+
AND used_at IS NULL
|
|
483
|
+
AND revoked_at IS NULL
|
|
484
|
+
AND expires_at > NOW()
|
|
485
|
+
ORDER BY created_at DESC
|
|
486
|
+
`;
|
|
487
|
+
const invitationRecords = await (0, pg_client_1.query)(db, invitationsSql, [organizationId]);
|
|
488
|
+
// Get all related users and organizations
|
|
489
|
+
const result = [];
|
|
490
|
+
for (const invitationRecord of invitationRecords) {
|
|
491
|
+
const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
|
|
492
|
+
const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
|
|
493
|
+
const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
|
|
494
|
+
const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
|
|
495
|
+
const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
|
|
496
|
+
if (!invitedByUserRecord || !organizationRecord)
|
|
497
|
+
continue;
|
|
498
|
+
result.push({
|
|
499
|
+
id: invitationRecord.id,
|
|
500
|
+
email: invitationRecord.email,
|
|
501
|
+
token: invitationRecord.token,
|
|
502
|
+
invitationMessage: invitationRecord.invitation_message,
|
|
503
|
+
invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
|
|
504
|
+
invitedByUserId: invitationRecord.invited_by_user_id,
|
|
505
|
+
shadowUserId: invitationRecord.shadow_user_id,
|
|
506
|
+
organizationId: invitationRecord.organization_id,
|
|
507
|
+
createdAt: invitationRecord.created_at,
|
|
508
|
+
expiresAt: invitationRecord.expires_at,
|
|
509
|
+
usedAt: invitationRecord.used_at,
|
|
510
|
+
revokedAt: invitationRecord.revoked_at,
|
|
511
|
+
invitedByUser: {
|
|
512
|
+
id: invitedByUserRecord.id,
|
|
513
|
+
givenName: invitedByUserRecord.given_name,
|
|
514
|
+
familyName: invitedByUserRecord.family_name,
|
|
515
|
+
},
|
|
516
|
+
shadowUser: shadowUserRecord,
|
|
517
|
+
organization: organizationRecord,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Get all invitations (pending, used, expired, revoked) for an organization
|
|
524
|
+
*/
|
|
525
|
+
async function getAllInvitationsByOrganization(db, organizationId) {
|
|
526
|
+
const invitationsSql = `
|
|
527
|
+
SELECT
|
|
528
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
529
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
530
|
+
created_at, expires_at, used_at, revoked_at
|
|
531
|
+
FROM authentication.invitations
|
|
532
|
+
WHERE organization_id = $1
|
|
533
|
+
ORDER BY created_at DESC
|
|
534
|
+
`;
|
|
535
|
+
const invitationRecords = await (0, pg_client_1.query)(db, invitationsSql, [organizationId]);
|
|
536
|
+
// Get all related users and organizations
|
|
537
|
+
const result = [];
|
|
538
|
+
for (const invitationRecord of invitationRecords) {
|
|
539
|
+
const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
|
|
540
|
+
const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
|
|
541
|
+
const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
|
|
542
|
+
const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
|
|
543
|
+
const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
|
|
544
|
+
if (!invitedByUserRecord || !organizationRecord)
|
|
545
|
+
continue;
|
|
546
|
+
result.push({
|
|
547
|
+
id: invitationRecord.id,
|
|
548
|
+
email: invitationRecord.email,
|
|
549
|
+
token: invitationRecord.token,
|
|
550
|
+
invitationMessage: invitationRecord.invitation_message,
|
|
551
|
+
invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
|
|
552
|
+
invitedByUserId: invitationRecord.invited_by_user_id,
|
|
553
|
+
shadowUserId: invitationRecord.shadow_user_id,
|
|
554
|
+
organizationId: invitationRecord.organization_id,
|
|
555
|
+
createdAt: invitationRecord.created_at,
|
|
556
|
+
expiresAt: invitationRecord.expires_at,
|
|
557
|
+
usedAt: invitationRecord.used_at,
|
|
558
|
+
revokedAt: invitationRecord.revoked_at,
|
|
559
|
+
invitedByUser: {
|
|
560
|
+
id: invitedByUserRecord.id,
|
|
561
|
+
givenName: invitedByUserRecord.given_name,
|
|
562
|
+
familyName: invitedByUserRecord.family_name,
|
|
563
|
+
},
|
|
564
|
+
shadowUser: shadowUserRecord,
|
|
565
|
+
organization: organizationRecord,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Check if a pending invitation exists for an email in an organization
|
|
572
|
+
*/
|
|
573
|
+
async function hasPendingInvitation(db, email, organizationId) {
|
|
574
|
+
const sql = `
|
|
575
|
+
SELECT id FROM authentication.invitations
|
|
576
|
+
WHERE email = $1
|
|
577
|
+
AND organization_id = $2
|
|
578
|
+
AND used_at IS NULL
|
|
579
|
+
AND revoked_at IS NULL
|
|
580
|
+
AND expires_at > NOW()
|
|
581
|
+
LIMIT 1
|
|
582
|
+
`;
|
|
583
|
+
const invitation = await (0, pg_client_1.queryOne)(db, sql, [email.toLowerCase().trim(), organizationId]);
|
|
584
|
+
return !!invitation;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Check if a shadow user has an active invitation for an organization
|
|
588
|
+
*/
|
|
589
|
+
async function hasActiveInvitationForShadowUser(db, shadowUserId, organizationId) {
|
|
590
|
+
const sql = `
|
|
591
|
+
SELECT id FROM authentication.invitations
|
|
592
|
+
WHERE shadow_user_id = $1
|
|
593
|
+
AND organization_id = $2
|
|
594
|
+
AND used_at IS NULL
|
|
595
|
+
AND revoked_at IS NULL
|
|
596
|
+
AND expires_at > NOW()
|
|
597
|
+
LIMIT 1
|
|
598
|
+
`;
|
|
599
|
+
const invitation = await (0, pg_client_1.queryOne)(db, sql, [shadowUserId, organizationId]);
|
|
600
|
+
return !!invitation;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Get all invitations sent by a specific user
|
|
604
|
+
*/
|
|
605
|
+
async function getInvitationsSentByUser(db, invitedByUserId) {
|
|
606
|
+
const invitationsSql = `
|
|
607
|
+
SELECT
|
|
608
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
609
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
610
|
+
created_at, expires_at, used_at, revoked_at
|
|
611
|
+
FROM authentication.invitations
|
|
612
|
+
WHERE invited_by_user_id = $1
|
|
613
|
+
ORDER BY created_at DESC
|
|
614
|
+
`;
|
|
615
|
+
const invitationRecords = await (0, pg_client_1.query)(db, invitationsSql, [invitedByUserId]);
|
|
616
|
+
// Get all related users and organizations
|
|
617
|
+
const result = [];
|
|
618
|
+
for (const invitationRecord of invitationRecords) {
|
|
619
|
+
const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
|
|
620
|
+
const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
|
|
621
|
+
// Get shadow user details with LinkedIn profile picture
|
|
622
|
+
const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
|
|
623
|
+
const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
|
|
624
|
+
const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
|
|
625
|
+
if (!invitedByUserRecord || !organizationRecord)
|
|
626
|
+
continue;
|
|
627
|
+
result.push({
|
|
628
|
+
id: invitationRecord.id,
|
|
629
|
+
email: invitationRecord.email,
|
|
630
|
+
token: invitationRecord.token,
|
|
631
|
+
invitationMessage: invitationRecord.invitation_message,
|
|
632
|
+
invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
|
|
633
|
+
invitedByUserId: invitationRecord.invited_by_user_id,
|
|
634
|
+
shadowUserId: invitationRecord.shadow_user_id,
|
|
635
|
+
organizationId: invitationRecord.organization_id,
|
|
636
|
+
createdAt: invitationRecord.created_at,
|
|
637
|
+
expiresAt: invitationRecord.expires_at,
|
|
638
|
+
usedAt: invitationRecord.used_at,
|
|
639
|
+
revokedAt: invitationRecord.revoked_at,
|
|
640
|
+
invitedByUser: {
|
|
641
|
+
id: invitedByUserRecord.id,
|
|
642
|
+
givenName: invitedByUserRecord.given_name,
|
|
643
|
+
familyName: invitedByUserRecord.family_name,
|
|
644
|
+
},
|
|
645
|
+
shadowUser: shadowUserRecord,
|
|
646
|
+
organization: organizationRecord,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Get all pending invitations sent by a specific user
|
|
653
|
+
*/
|
|
654
|
+
async function getPendingInvitationsSentByUser(db, invitedByUserId) {
|
|
655
|
+
const invitationsSql = `
|
|
656
|
+
SELECT
|
|
657
|
+
id, email, token, invitation_message, invited_user_permissions_role,
|
|
658
|
+
invited_by_user_id, shadow_user_id, organization_id,
|
|
659
|
+
created_at, expires_at, used_at, revoked_at
|
|
660
|
+
FROM authentication.invitations
|
|
661
|
+
WHERE invited_by_user_id = $1
|
|
662
|
+
AND used_at IS NULL
|
|
663
|
+
AND revoked_at IS NULL
|
|
664
|
+
AND expires_at > NOW()
|
|
665
|
+
ORDER BY created_at DESC
|
|
666
|
+
`;
|
|
667
|
+
const invitationRecords = await (0, pg_client_1.query)(db, invitationsSql, [invitedByUserId]);
|
|
668
|
+
// Get all related users and organizations
|
|
669
|
+
const result = [];
|
|
670
|
+
for (const invitationRecord of invitationRecords) {
|
|
671
|
+
const userSql = `SELECT id, given_name, family_name FROM public.users WHERE id = $1 LIMIT 1`;
|
|
672
|
+
const invitedByUserRecord = await (0, pg_client_1.queryOne)(db, userSql, [invitationRecord.invited_by_user_id]);
|
|
673
|
+
const orgSql = `SELECT id, name FROM public.organizations WHERE id = $1 LIMIT 1`;
|
|
674
|
+
const organizationRecord = await (0, pg_client_1.queryOne)(db, orgSql, [invitationRecord.organization_id]);
|
|
675
|
+
const shadowUserRecord = await fetchShadowUserWithProfilePicture(db, invitationRecord.shadow_user_id);
|
|
676
|
+
if (!invitedByUserRecord || !organizationRecord)
|
|
677
|
+
continue;
|
|
678
|
+
result.push({
|
|
679
|
+
id: invitationRecord.id,
|
|
680
|
+
email: invitationRecord.email,
|
|
681
|
+
token: invitationRecord.token,
|
|
682
|
+
invitationMessage: invitationRecord.invitation_message,
|
|
683
|
+
invitedUserPermissionsRole: invitationRecord.invited_user_permissions_role,
|
|
684
|
+
invitedByUserId: invitationRecord.invited_by_user_id,
|
|
685
|
+
shadowUserId: invitationRecord.shadow_user_id,
|
|
686
|
+
organizationId: invitationRecord.organization_id,
|
|
687
|
+
createdAt: invitationRecord.created_at,
|
|
688
|
+
expiresAt: invitationRecord.expires_at,
|
|
689
|
+
usedAt: invitationRecord.used_at,
|
|
690
|
+
revokedAt: invitationRecord.revoked_at,
|
|
691
|
+
invitedByUser: {
|
|
692
|
+
id: invitedByUserRecord.id,
|
|
693
|
+
givenName: invitedByUserRecord.given_name,
|
|
694
|
+
familyName: invitedByUserRecord.family_name,
|
|
695
|
+
},
|
|
696
|
+
shadowUser: shadowUserRecord,
|
|
697
|
+
organization: organizationRecord,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
// =============================================================================
|
|
703
|
+
// CLEANUP
|
|
704
|
+
// =============================================================================
|
|
705
|
+
/**
|
|
706
|
+
* Delete expired invitations older than specified days
|
|
707
|
+
*/
|
|
708
|
+
async function cleanupExpiredInvitations(db, olderThanDays = 30) {
|
|
709
|
+
const sql = `
|
|
710
|
+
DELETE FROM authentication.invitations
|
|
711
|
+
WHERE (NOW() - expires_at) > INTERVAL '${olderThanDays} days'
|
|
712
|
+
AND used_at IS NULL
|
|
713
|
+
RETURNING id
|
|
714
|
+
`;
|
|
715
|
+
const result = await (0, pg_client_1.query)(db, sql, []);
|
|
716
|
+
return result.length;
|
|
717
|
+
}
|
|
718
|
+
// =============================================================================
|
|
719
|
+
// HELPER FUNCTIONS
|
|
720
|
+
// =============================================================================
|
|
721
|
+
/**
|
|
722
|
+
* Ensure contact info exists for invitation email
|
|
723
|
+
*/
|
|
724
|
+
async function ensureContactInfoForInvitation(db, email, shadowUserId) {
|
|
725
|
+
// Try to find existing contact info for this email with INVITATION source
|
|
726
|
+
const existingSql = `
|
|
727
|
+
SELECT id FROM public.contact_infos
|
|
728
|
+
WHERE value = $1
|
|
729
|
+
AND type = 'EMAIL'
|
|
730
|
+
AND source = 'INVITATION'
|
|
731
|
+
LIMIT 1
|
|
732
|
+
`;
|
|
733
|
+
const existingContactInfo = await (0, pg_client_1.queryOne)(db, existingSql, [email]);
|
|
734
|
+
if (existingContactInfo) {
|
|
735
|
+
return existingContactInfo.id;
|
|
736
|
+
}
|
|
737
|
+
// Create new contact info record
|
|
738
|
+
const insertSql = `
|
|
739
|
+
INSERT INTO public.contact_infos (user_id, value, type, source, created_at)
|
|
740
|
+
VALUES ($1, $2, 'EMAIL', 'INVITATION', NOW())
|
|
741
|
+
RETURNING id
|
|
742
|
+
`;
|
|
743
|
+
const contactInfoResult = await (0, pg_client_1.queryOne)(db, insertSql, [shadowUserId, email]);
|
|
744
|
+
if (!contactInfoResult?.id) {
|
|
745
|
+
throw new Error("CONTACT_INFO_NOT_FOUND_FOR_EMAIL");
|
|
746
|
+
}
|
|
747
|
+
return contactInfoResult.id;
|
|
748
|
+
}
|
|
749
|
+
//# sourceMappingURL=invitation-operations.js.map
|