@cocreate/users 1.39.3 → 1.40.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/src/server.js CHANGED
@@ -1,244 +1,340 @@
1
- class CoCreateUser {
2
- constructor(crud) {
3
- this.wsManager = crud.wsManager;
4
- this.crud = crud;
5
- this.init();
6
- }
7
-
8
- init() {
9
- if (this.wsManager) {
10
- this.wsManager.on("signUp", (data) => this.signUp(data));
11
- this.wsManager.on("signIn", (data) => this.signIn(data));
12
- this.wsManager.on("userStatus", (data) => this.userStatus(data));
13
- this.wsManager.on("checkSession", (data) =>
14
- this.checkSession(data)
15
- );
16
- this.wsManager.on("inviteUser", (data) => this.inviteUser(data));
17
- this.wsManager.on("acceptInvite", (data) =>
18
- this.acceptInvite(data)
19
- );
20
- this.wsManager.on("forgotPassword", (data) =>
21
- this.forgotPassword(data)
22
- );
23
- this.wsManager.on("resetPassword", (data) =>
24
- this.resetPassword(data)
25
- );
26
- }
27
- }
28
-
29
- async signUp(data) {
30
- try {
31
- let response = {
32
- socket: data.socket,
33
- host: data.host,
34
- method: "signUp",
35
- success: false,
36
- message: "signUp failed",
37
- organization_id: data.organization_id,
38
- uid: data.uid
39
- };
40
-
41
- if (data.user) {
42
- data.user.method = "object.create";
43
- data.user.host = data.host;
44
- if (!data.user.organization_id) {
45
- data.user.organization_id = data.organization_id;
46
- data.userKey.organization_id = data.organization_id;
47
- }
48
- let createdUser = await this.crud.send(data.user);
49
- if (data.userKey && createdUser.object[0] && createdUser.object[0]._id) {
50
- data.userKey.object.key = createdUser.object[0]._id;
51
- }
52
- }
53
-
54
- if (data.userKey) {
55
- data.userKey.method = "object.create";
56
- data.userKey.host = data.host;
57
- if (!data.userKey.organization_id) {
58
- data.userKey.organization_id = data.organization_id;
59
- }
60
- await this.crud.send(data.userKey);
61
- }
62
-
63
- response.success = true;
64
- response.message = "signUp successful";
65
-
66
- this.wsManager.send(response);
67
- } catch (error) {
68
- console.log("signup error", error);
69
- }
70
- }
71
-
72
- /**
73
- data = {
74
- namespace: string,
75
- array: string,
76
- data: object,
77
- eId: string,
78
- key: string,
79
- organization_id: string
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ ********************************************************************************/
18
+
19
+ // Commercial Licensing Information:
20
+ // For commercial use of this software without the copyleft provisions of the AGPLv3,
21
+ // you must obtain a commercial license from CoCreate LLC.
22
+ // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
23
+
24
+ import server from '@cocreate/server';
25
+
26
+ /**
27
+ * Handles the user sign-up process by performing identity validation,
28
+ * contact record synchronization, and credential attachment.
29
+ */
30
+ async function signUp(data) {
31
+ try {
32
+ let response = {
33
+ socket: data.socket,
34
+ host: data.host,
35
+ method: "signUp",
36
+ success: false,
37
+ message: "signUp failed",
38
+ organization_id: data.organization_id,
39
+ uid: data.uid
40
+ };
41
+
42
+ const targetEmail = data.user?.object?.email;
43
+ const targetId = data.user?.object?._id;
44
+
45
+ // --- STEP 1: The Ultimate Gatekeeper Check ---
46
+ // We look in the 'keys' array to see if this email OR this ID is already registered.
47
+ if (!server || !server.crud) return;
48
+
49
+ const keyCheck = await server.crud.send({
50
+ method: "object.read",
51
+ array: "keys",
52
+ $filter: {
53
+ limit: 1,
54
+ query: {
55
+ $or: [
56
+ { "email": targetEmail },
57
+ { "key": targetId }
58
+ ]
59
+ }
60
+ }
61
+ });
62
+
63
+ // Detailed error messaging for the user
64
+ if (keyCheck?.object?.length > 0) {
65
+ const match = keyCheck.object[0];
66
+
67
+ if (match.email === targetEmail && match.key !== targetId) {
68
+ response.message = "Email is already in use with another account";
69
+ } else if (match.key === targetId) {
70
+ response.message = "You already have an account, try signing in instead";
71
+ } else {
72
+ response.message = "An account with these details already exists";
73
+ }
74
+
75
+ if (server.wsManager) {
76
+ server.wsManager.send(response);
77
+ }
78
+ return; // Exit early: Gatekeeper blocked the sign-up.
80
79
  }
81
- **/
82
- async signIn(data) {
83
- const self = this;
84
- try {
85
- data.method = "object.read";
86
- data.array = "keys";
87
- let socket = data.socket;
88
- delete data.socket;
89
-
90
- // TODO: Enforce query from array keys $filter.query = {email, phone, username, password, key }
91
- // Get crud properties from returned key and enforce query $filter.query = {database, array, email, phone, username, password, key }
92
- // or use webhook to update keys credintials when user updates ther user
93
- // Also enforce roles by webhook else role could be spoofed,
94
- // Also enforce admin privlige
95
-
96
- this.crud.send(data).then(async (data) => {
97
- let response = {
98
- socket,
99
- host: data.host,
100
- method: "signIn",
101
- success: false,
102
- message: "signIn failed",
103
- userStatus: "offline",
104
- organization_id: data.organization_id,
105
- uid: data.uid
106
- };
107
-
108
- if (
109
- data.object[0] &&
110
- data.object[0]._id &&
111
- self.wsManager.authenticate
112
- ) {
113
- const user_id = data.object[0].key;
114
- // TODO: get key to then get crud properties from key to intiate signin
115
- // const { storage, database, array, key } = data.object[0];
116
- // data = await this.crud.send(data);
117
- const token = self.wsManager.authenticate.encodeToken(
118
- data.organization_id,
119
- user_id,
120
- data.clientId
121
- );
122
-
123
- if (token && token != "null") {
124
- socket.user_id = user_id;
125
- response.success = true;
126
- response.message = "signIn successful";
127
- response.userStatus = "online";
128
- response.user_id = user_id;
129
- response.token = token;
130
- }
131
- }
132
- self.wsManager.send(response);
133
- self.wsManager.send({
134
- socket,
135
- host: data.host,
136
- method: "updateUserStatus",
137
- user_id: response.user_id,
138
- userStatus: response.userStatus,
139
- organization_id: response.organization_id
140
- });
141
- });
142
- } catch (error) {
143
- console.log("signIn failed", error);
144
- }
145
- }
146
-
147
- /**
148
- * status: 'online/offline/idle'
149
- */
150
- async userStatus(data) {
151
- const self = this;
152
- try {
153
- if (data.user_id && data.userStatus) {
154
- data.array = "users";
155
- data["object"] = {
156
- _id: data.user_id,
157
- userStatus: data.userStatus
158
- };
159
-
160
- data.method = "object.update";
161
- data = await this.crud.send(data);
162
-
163
- if (data.socket)
164
- self.wsManager.send({
165
- socket: data.socket,
166
- method: "updateUserStatus",
167
- host: data.host,
168
- user_id: data.user_id,
169
- clientId: data.clientId,
170
- userStatus: data.userStatus,
171
- token: data.token,
172
- organization_id:
173
- data.organization_id || socket.organization_id
174
- });
175
- } else if (data.socket)
176
- data.socket.send(
177
- JSON.stringify({
178
- method: "updateUserStatus",
179
- host: data.host,
180
- user_id: data.user_id,
181
- userStatus: data.userStatus,
182
- clientId: data.clientId,
183
- token: data.token,
184
- organization_id:
185
- data.organization_id || socket.organization_id
186
- })
187
- );
188
- } catch (error) {
189
- console.log("userStatus error");
190
- }
191
- }
192
-
193
- async checkSession(data) {
194
- try {
195
- data.method = "updateUserStatus";
196
-
197
- if (!data.socket.user_id) data.userStatus = "offline";
198
- else data.userStatus = "online";
199
-
200
- this.wsManager.send(data);
201
- } catch (error) {
202
- console.log("checkSession error");
203
- }
204
- }
205
-
206
- // TODO: email or phone param refrencing the object to execute via api
207
- async inviteUser(data) {
208
- try {
209
- const inviteId = this.crud.ObjectId().toString();
210
- let uid = data.uid;
211
- delete data.uid;
212
-
213
- data.method = "object.update";
214
- data.array = "users";
215
- data.object = {
216
- _id: data.user_id,
217
- "$push.invitations": inviteId,
218
- "$addToSet.members": data.email
219
- };
220
-
221
- // TODO: upodateDB is a temp solution to by pass crud.sync clientId as all crud methods are ignored fro same clientid except for read
222
- data.updateDB = true;
223
-
224
- data = await this.crud.send(data);
225
-
226
- let invitee = await this.crud.send({
227
- method: "object.read",
228
- host: data.host,
229
- array: "users",
230
- $filter: {
231
- query: { email: data.email },
232
- limit: 1
233
- },
234
- organization_id: data.organization_id
235
- });
236
-
237
- if (invitee.object[0]) {
238
- invitee = invitee.object[0]._id;
239
- } else invitee = "";
240
-
241
- let htmlBody = `
80
+
81
+ // --- STEP 2: Resolve or Create the Contact ---
82
+ if (data.user) {
83
+ data.user.method = "object.create";
84
+ data.user.host = data.host;
85
+
86
+ if (!data.user.organization_id) {
87
+ data.user.organization_id = data.organization_id;
88
+ }
89
+
90
+ if (data.user.object) {
91
+ delete data.user.object.password; // Remove password attribute to avoid plain text database leakage
92
+ }
93
+
94
+ let userResult = await server.crud.send(data.user);
95
+
96
+ // --- STEP 3: Create the Key ---
97
+ if (data.userKey && userResult.object?.[0]?._id) {
98
+ const resolvedUserId = userResult.object[0]._id;
99
+
100
+ data.userKey.method = "object.create";
101
+ data.userKey.host = data.host;
102
+ data.userKey.object.key = resolvedUserId;
103
+ data.userKey.object.email = targetEmail;
104
+
105
+ if (!data.userKey.organization_id) {
106
+ data.userKey.organization_id = data.organization_id;
107
+ }
108
+
109
+ // Get the result and check for an ID to confirm success
110
+ let createdKey = await server.crud.send(data.userKey);
111
+
112
+ if (createdKey.object?.[0]?._id) {
113
+ response.success = true;
114
+ response.message = "signUp successful";
115
+ } else {
116
+ response.message = "Account creation failed at credential stage";
117
+ }
118
+ } else {
119
+ response.message = "Account creation failed at user stage";
120
+ }
121
+ }
122
+
123
+ if (server.wsManager) {
124
+ server.wsManager.send(response);
125
+ }
126
+
127
+ } catch (error) {
128
+ console.error("signup error", error);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Executes credential matching queries, assigns socket-level identifiers,
134
+ * and encodes secure session web tokens.
135
+ */
136
+ async function signIn(data) {
137
+ try {
138
+ if (!server || !server.crud) return;
139
+
140
+ data.method = "object.read";
141
+ data.array = "keys";
142
+ let socket = data.socket;
143
+ delete data.socket;
144
+
145
+ const result = await server.crud.send(data);
146
+
147
+ let response = {
148
+ socket,
149
+ host: result.host,
150
+ method: "signIn",
151
+ success: false,
152
+ message: "signIn failed",
153
+ userStatus: "offline",
154
+ organization_id: result.organization_id,
155
+ uid: result.uid
156
+ };
157
+
158
+ if (
159
+ result.object[0] &&
160
+ result.object[0]._id &&
161
+ server.wsManager &&
162
+ server.authenticate
163
+ ) {
164
+ const user_id = result.object[0].key;
165
+ const token = server.authenticate.encodeToken(
166
+ result.organization_id,
167
+ user_id,
168
+ result.clientId
169
+ );
170
+
171
+ if (token && token !== "null") {
172
+ socket.user_id = user_id;
173
+ response.success = true;
174
+ response.message = "signIn successful";
175
+ response.userStatus = "online";
176
+ response.user_id = user_id;
177
+ response.token = token;
178
+ }
179
+ }
180
+
181
+ if (server.wsManager) {
182
+ server.wsManager.send(response);
183
+ server.wsManager.send({
184
+ socket,
185
+ host: result.host,
186
+ method: "updateUserStatus",
187
+ user_id: response.user_id,
188
+ userStatus: response.userStatus,
189
+ organization_id: response.organization_id
190
+ });
191
+ }
192
+ } catch (error) {
193
+ console.error("signIn failed", error);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Updates dynamic user activity indicators ('online', 'offline', 'idle').
199
+ */
200
+ async function userStatus(data) {
201
+ try {
202
+ if (!server || !server.crud) return;
203
+
204
+ if (data.user_id && data.userStatus) {
205
+ let targetArray = "users"; // default fallback
206
+ let keyData = await server.crud.send({
207
+ method: "object.read",
208
+ host: data.host,
209
+ array: "keys",
210
+ $filter: {
211
+ query: { key: data.user_id },
212
+ limit: 1
213
+ },
214
+ organization_id: data.organization_id || (data.socket && data.socket.organization_id)
215
+ });
216
+
217
+ if (keyData && keyData.object && keyData.object.length > 0 && keyData.object[0].array) {
218
+ targetArray = keyData.object[0].array;
219
+ }
220
+
221
+ data.array = targetArray;
222
+ data["object"] = {
223
+ _id: data.user_id,
224
+ userStatus: data.userStatus
225
+ };
226
+
227
+ data.method = "object.update";
228
+ const updatedData = await server.crud.send(data);
229
+
230
+ if (updatedData.socket && server.wsManager) {
231
+ server.wsManager.send({
232
+ socket: updatedData.socket,
233
+ method: "updateUserStatus",
234
+ host: updatedData.host,
235
+ user_id: updatedData.user_id,
236
+ clientId: updatedData.clientId,
237
+ userStatus: updatedData.userStatus,
238
+ token: updatedData.token,
239
+ organization_id:
240
+ updatedData.organization_id || (updatedData.socket && updatedData.socket.organization_id)
241
+ });
242
+ }
243
+ } else if (data.socket) {
244
+ data.socket.send(
245
+ JSON.stringify({
246
+ method: "updateUserStatus",
247
+ host: data.host,
248
+ user_id: data.user_id,
249
+ userStatus: data.userStatus,
250
+ clientId: data.clientId,
251
+ token: data.token,
252
+ organization_id:
253
+ data.organization_id || (data.socket && data.socket.organization_id)
254
+ })
255
+ );
256
+ }
257
+ } catch (error) {
258
+ console.error("userStatus error", error);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Quickly checks for valid runtime credentials assigned to active client connections.
264
+ */
265
+ async function checkSession(data) {
266
+ try {
267
+ data.method = "updateUserStatus";
268
+
269
+ if (!data.socket.user_id) data.userStatus = "offline";
270
+ else data.userStatus = "online";
271
+
272
+ if (server && server.wsManager) {
273
+ server.wsManager.send(data);
274
+ }
275
+ } catch (error) {
276
+ console.error("checkSession error", error);
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Creates, formats, and dispatches email invitations to new team collaborators.
282
+ */
283
+ async function inviteUser(data) {
284
+ try {
285
+ if (!server || !server.crud) return;
286
+
287
+ const inviteId = server.crud.ObjectId().toString();
288
+ let uid = data.uid;
289
+ delete data.uid;
290
+
291
+ let targetArray = "users"; // default fallback
292
+ let keyData = await server.crud.send({
293
+ method: "object.read",
294
+ host: data.host,
295
+ array: "keys",
296
+ $filter: {
297
+ query: { key: data.user_id },
298
+ limit: 1
299
+ },
300
+ organization_id: data.organization_id || (data.socket && data.socket.organization_id)
301
+ });
302
+
303
+ if (keyData && keyData.object && keyData.object.length > 0 && keyData.object[0].array) {
304
+ targetArray = keyData.object[0].array;
305
+ }
306
+
307
+ data.method = "object.update";
308
+ data.array = targetArray;
309
+ data.object = {
310
+ _id: data.user_id,
311
+ "$push.invitations": inviteId,
312
+ "$addToSet.members": data.email
313
+ };
314
+
315
+ // Bypass crud.sync check to update across local clients simultaneously
316
+ data.updateDB = true;
317
+
318
+ const updatedData = await server.crud.send(data);
319
+
320
+ let invitee = await server.crud.send({
321
+ method: "object.read",
322
+ host: updatedData.host,
323
+ array: "users",
324
+ $filter: {
325
+ query: { email: updatedData.email },
326
+ limit: 1
327
+ },
328
+ organization_id: updatedData.organization_id
329
+ });
330
+
331
+ if (invitee.object && invitee.object[0]) {
332
+ invitee = invitee.object[0]._id;
333
+ } else {
334
+ invitee = "";
335
+ }
336
+
337
+ let htmlBody = `
242
338
  <html>
243
339
  <head>
244
340
  <title>Welcome to Yellow Oracle!</title>
@@ -246,164 +342,174 @@ class CoCreateUser {
246
342
  <body>
247
343
  <p>Hello,</p>
248
344
 
249
- <p>You have been invited to join ${data.name} on Yellow Oracle.</p>
345
+ <p>You have been invited to join ${updatedData.name} on Yellow Oracle.</p>
250
346
 
251
- <p><a href="${data.origin}${data.path}?email=${data.email}&token=${inviteId}&user_id=${invitee}&name=${data.name}" style="color: #ffffff; background-color: #FFD700; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Accept Your Invitation</a></p>
347
+ <p><a href="${updatedData.origin}${updatedData.path}?email=${updatedData.email}&token=${inviteId}&user_id=${invitee}&name=${updatedData.name}" style="color: #ffffff; background-color: #FFD700; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Accept Your Invitation</a></p>
252
348
 
253
- <p>Please note, this invitation link will expire in 48 hours. We encourage you to accept it soon to begin your journey with ${data.name} on Yellow Oracle.</p>
349
+ <p>Please note, this invitation link will expire in 48 hours. We encourage you to accept it soon to begin your journey with ${updatedData.name} on Yellow Oracle.</p>
254
350
 
255
351
  <p>If the button above doesn't work, you can copy and paste the following URL into your web browser:</p>
256
- <p><a href="${data.origin}${data.path}?email=${data.email}&token=${inviteId}&user_id=${invitee}&name=${data.name}">${data.origin}${data.path}?email=${data.email}&token=${inviteId}&user_id=${invitee}&name=${data.name}</a></p>
352
+ <p><a href="${updatedData.origin}${updatedData.path}?email=${updatedData.email}&token=${inviteId}&user_id=${invitee}&name=${updatedData.name}">${updatedData.origin}${updatedData.path}?email=${updatedData.email}&token=${inviteId}&user_id=${invitee}&name=${updatedData.name}</a></p>
257
353
 
258
- <p>If you received this invitation by mistake or have any questions, please don't hesitate to get in touch with our support team at <a href="mailto:support@${data.hostname}">support@${data.hostname}</a>.</p>
354
+ <p>If you received this invitation by mistake or have any questions, please don't hesitate to get in touch with our support team at <a href="mailto:support@${updatedData.hostname}">support@${updatedData.hostname}</a>.</p>
259
355
 
260
- <p>We look forward to welcoming you to ${data.name}'s Yellow Oracle account!</p>
356
+ <p>We look forward to welcoming you to ${updatedData.name}'s Yellow Oracle account!</p>
261
357
 
262
358
  </body>
263
- </html>
359
+ </html>
264
360
  `;
265
361
 
266
- let email = {
267
- method: "postmark.sendEmail",
268
- host: data.host,
269
- postmark: {
270
- From: data.from,
271
- To: data.email,
272
- Subject: `${data.name} has invited you to join them on Yellow Oracle`,
273
- HtmlBody: htmlBody,
274
- TextBody:
275
- "Hello, \n\nWe received a request to reset the password for your account.If you did not make this request, please ignore this email.Otherwise, you can reset your password by copying and pasting the following link into your browser: https://example.com/reset-password\n\nThis link will expire in 24 hours for your security.\n\nNeed more help? Our support team is here for you at support@example.com.\n\nThank you for using our services!\n\nBest regards,\nThe [Your Company] Team",
276
- MessageStream: "outbound"
277
- },
278
- organization_id: data.organization_id
279
- };
280
-
281
- // TODO: wsManager.emit('postmark', email) needs to await response
282
- this.wsManager.emit("postmark", email);
283
-
284
- let response = {
285
- socket: data.socket,
286
- host: data.host,
287
- method: "inviteUser",
288
- success: true,
289
- message: "Succesfully sent invite",
290
- organization_id: data.organization_id,
291
- uid
292
- };
293
-
294
- this.wsManager.send(response);
295
- } catch (error) {
296
- data.error = error;
297
- this.wsManager.send(data);
298
-
299
- console.log("Invite User failed", error);
300
- }
301
- }
302
-
303
- async acceptInvite(data) {
304
- const self = this;
305
- try {
306
- if (!data.token || !data.user_id) {
307
- // TODO: handle error
308
- return;
309
- }
310
-
311
- let uid = data.uid;
312
- delete data.uid;
313
-
314
- data.method = "object.update";
315
- data.array = "users";
316
- data.object = {
317
- "$pull.invitations": data.token,
318
- "$pull.members": data.email
319
- };
320
- data.$filter = {
321
- query: {
322
- invitations: { $in: [data.token] },
323
- members: { $in: [data.email] }
324
- },
325
- limit: 2
326
- };
327
-
328
- // TODO: upodateDB is a temp solution to by pass crud.sync clientId as all crud methods are ignored fro same clientid except for read
329
- data.updateDB = true;
330
-
331
- data = await this.crud.send(data);
332
-
333
- let response = {
334
- socket: data.socket,
335
- host: data.host,
336
- method: "acceptInvite",
337
- success: false,
338
- message:
339
- "The token is invalid or has expired. However, you can still access your account. Please request a new invite to proceed, and feel free to explore the available features while you wait.",
340
- organization_id: data.organization_id,
341
- uid
342
- };
343
-
344
- for (let object of data.object) {
345
- if (object._id) {
346
- delete data.$filter;
347
- data.object = {
348
- _id: object._id,
349
- "$addToSet.members": data.user_id
350
- };
351
- data = await this.crud.send(data);
352
- this.crud.send({
353
- method: "object.update",
354
- host: data.host,
355
- array: "users",
356
- object: {
357
- _id: data.user_id,
358
- memberAccount: object._id,
359
- subscription: "6571fe530c48ef6970900a82"
360
- }
361
- });
362
- response.success = true;
363
- response.message = "Invite Accepted";
364
- break;
365
- }
366
- }
367
-
368
- self.wsManager.send(response);
369
- } catch (error) {
370
- console.log("Accept invite failed", error);
371
- }
372
- }
373
-
374
- // TODO: email or phone param refrencing the object to execute via api
375
- async forgotPassword(data) {
376
- const self = this;
377
- try {
378
- const recoveryId = this.crud.ObjectId().toString();
379
- let socket = data.socket;
380
- delete data.socket;
381
-
382
- data.method = "object.update";
383
- data.array = "keys";
384
- data.object = { recoveryId };
385
- data.$filter = {
386
- query: { email: data.email },
387
- limit: 1
388
- };
389
-
390
- this.crud.send(data).then(async (data) => {
391
- let response = {
392
- socket,
393
- host: data.host,
394
- method: "forgotPassword",
395
- success: false,
396
- message: "Email does not exist",
397
- organization_id: data.organization_id,
398
- uid: data.uid
399
- };
400
-
401
- for (let object of data.object) {
402
- if (object._id) {
403
- // TODO: sendEmail
404
- response.success = true;
405
- response.message = "Email Sent";
406
- let htmlBody = `
362
+ let email = {
363
+ method: "postmark.sendEmail",
364
+ host: updatedData.host,
365
+ postmark: {
366
+ From: updatedData.from,
367
+ To: updatedData.email,
368
+ Subject: `${updatedData.name} has invited you to join them on Yellow Oracle`,
369
+ HtmlBody: htmlBody,
370
+ TextBody:
371
+ "Hello, \n\nWe received a request to join Yellow Oracle. Please copy and paste the invitation link directly to proceed.",
372
+ MessageStream: "outbound"
373
+ },
374
+ organization_id: updatedData.organization_id
375
+ };
376
+
377
+ if (server.wsManager) {
378
+ server.wsManager.emit("postmark", email);
379
+ }
380
+
381
+ let response = {
382
+ socket: updatedData.socket,
383
+ host: updatedData.host,
384
+ method: "inviteUser",
385
+ success: true,
386
+ message: "Successfully sent invite",
387
+ organization_id: updatedData.organization_id,
388
+ uid
389
+ };
390
+
391
+ if (server.wsManager) {
392
+ server.wsManager.send(response);
393
+ }
394
+ } catch (error) {
395
+ data.error = error;
396
+ if (server && server.wsManager) {
397
+ server.wsManager.send(data);
398
+ }
399
+ console.error("Invite User failed", error);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Validates outstanding team tokens and connects user accounts to parent environments.
405
+ */
406
+ async function acceptInvite(data) {
407
+ try {
408
+ if (!server || !server.crud) return;
409
+ if (!data.token || !data.user_id) {
410
+ return;
411
+ }
412
+
413
+ let uid = data.uid;
414
+ delete data.uid;
415
+
416
+ data.method = "object.update";
417
+ data.array = "users";
418
+ data.object = {
419
+ "$pull.invitations": data.token,
420
+ "$pull.members": data.email
421
+ };
422
+ data.$filter = {
423
+ query: {
424
+ invitations: { $in: [data.token] },
425
+ members: { $in: [data.email] }
426
+ },
427
+ limit: 2
428
+ };
429
+
430
+ data.updateDB = true;
431
+
432
+ const updatedData = await server.crud.send(data);
433
+
434
+ let response = {
435
+ socket: updatedData.socket,
436
+ host: updatedData.host,
437
+ method: "acceptInvite",
438
+ success: false,
439
+ message:
440
+ "The token is invalid or has expired. However, you can still access your account. Please request a new invite to proceed, and feel free to explore the available features while you wait.",
441
+ organization_id: updatedData.organization_id,
442
+ uid
443
+ };
444
+
445
+ for (let object of updatedData.object) {
446
+ if (object._id) {
447
+ delete updatedData.$filter;
448
+ updatedData.object = {
449
+ _id: object._id,
450
+ "$addToSet.members": updatedData.user_id
451
+ };
452
+ const finalData = await server.crud.send(updatedData);
453
+ await server.crud.send({
454
+ method: "object.update",
455
+ host: finalData.host,
456
+ array: "users",
457
+ object: {
458
+ _id: finalData.user_id,
459
+ memberAccount: object._id,
460
+ subscription: "6571fe530c48ef6970900a82"
461
+ }
462
+ });
463
+ response.success = true;
464
+ response.message = "Invite Accepted";
465
+ break;
466
+ }
467
+ }
468
+
469
+ if (server.wsManager) {
470
+ server.wsManager.send(response);
471
+ }
472
+ } catch (error) {
473
+ console.error("Accept invite failed", error);
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Constructs recovery parameters and sends secure password-reset tokens to verified accounts.
479
+ */
480
+ async function forgotPassword(data) {
481
+ try {
482
+ if (!server || !server.crud) return;
483
+
484
+ const recoveryId = server.crud.ObjectId().toString();
485
+ let socket = data.socket;
486
+ delete data.socket;
487
+
488
+ data.method = "object.update";
489
+ data.array = "keys";
490
+ data.object = { recoveryId };
491
+ data.$filter = {
492
+ query: { email: data.email },
493
+ limit: 1
494
+ };
495
+
496
+ const result = await server.crud.send(data);
497
+
498
+ let response = {
499
+ socket,
500
+ host: result.host,
501
+ method: "forgotPassword",
502
+ success: false,
503
+ message: "Email does not exist",
504
+ organization_id: result.organization_id,
505
+ uid: result.uid
506
+ };
507
+
508
+ for (let object of result.object) {
509
+ if (object._id) {
510
+ response.success = true;
511
+ response.message = "Email Sent";
512
+ let htmlBody = `
407
513
  <html>
408
514
  <head>
409
515
  <title>Reset Your Password</title>
@@ -413,87 +519,117 @@ class CoCreateUser {
413
519
 
414
520
  <p>We received a request to reset the password for your account. If you did not make this request, please ignore this email. Otherwise, you can reset your password using the link below.</p>
415
521
 
416
- <p><a href="${data.origin}${data.path}?email=${data.email}&token=${recoveryId}&recoveyId=${recoveryId}" style="color: #ffffff; background-color: #007bff; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Reset My Password</a></p>
522
+ <p><a href="${result.origin}${result.path}?email=${result.email}&token=${recoveryId}&recoveyId=${recoveryId}" style="color: #ffffff; background-color: #007bff; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Reset My Password</a></p>
417
523
 
418
524
  <p>This link will expire in 24 hours for your security.</p>
419
525
 
420
526
  <p>If you're having trouble with the button above, copy and paste the URL below into your web browser:</p>
421
- <p><a href="${data.origin}${data.path}?email=${data.email}&token=${recoveryId}&recoveyId=${recoveryId}"">${data.origin}${data.path}?email=${data.email}&token=${recoveryId}&recoveyId=${recoveryId}"</a></p>
527
+ <p><a href="${result.origin}${result.path}?email=${result.email}&token=${recoveryId}&recoveyId=${recoveryId}">${result.origin}${result.path}?email=${result.email}&token=${recoveryId}&recoveyId=${recoveryId}</a></p>
422
528
 
423
- <p>Need more help? Our support team is here for you. Contact us at <a href="mailto:support@${data.hostname}">support@${data.hostname}</a>.</p>
529
+ <p>Need more help? Our support team is here for you. Contact us at <a href="mailto:support@${result.hostname}">support@${result.hostname}</a>.</p>
424
530
 
425
531
  <p>Thank you for using our services!</p>
426
532
 
427
533
  </body>
428
534
  </html>
429
535
  `;
430
- let email = {
431
- method: "postmark.sendEmail",
432
- host: data.host,
433
- postmark: {
434
- From: data.from,
435
- To: data.email,
436
- Subject: "Reset Your Password Easily",
437
- HtmlBody: htmlBody,
438
- TextBody:
439
- "Hello, \n\nWe received a request to reset the password for your account. If you did not make this request, please ignore this email. Otherwise, you can reset your password by copying and pasting the following link into your browser: https://example.com/reset-password\n\nThis link will expire in 24 hours for your security.\n\nNeed more help? Our support team is here for you at support@example.com.\n\nThank you for using our services!\n\nBest regards,\nThe [Your Company] Team",
440
- MessageStream: "outbound"
441
- },
442
- organization_id: data.organization_id
443
- };
444
-
445
- // TODO: wsManager.emit('postmark', email) needs to await response
446
- self.wsManager.emit("postmark", email);
447
-
448
- break;
449
- }
450
- }
451
-
452
- self.wsManager.send(response);
453
- });
454
- } catch (error) {
455
- console.log("Forgot Password failed", error);
456
- }
457
- }
458
-
459
- async resetPassword(data) {
460
- const self = this;
461
- try {
462
- if (!data.email || !data.password || !data.token) return;
463
-
464
- data.method = "object.update";
465
- data.array = "keys";
466
- data.object = { password: data.password, recoveryId: "" };
467
- data.$filter = {
468
- query: { email: data.email, recoveryId: data.token },
469
- limit: 1
470
- };
471
-
472
- this.crud.send(data).then(async (data) => {
473
- let response = {
474
- socket: data.socket,
475
- host: data.host,
476
- method: "resetPassword",
477
- success: false,
478
- message: "Token is invalid or has expired",
479
- organization_id: data.organization_id,
480
- uid: data.uid
481
- };
482
-
483
- for (let object of data.object) {
484
- if (object._id) {
485
- response.success = true;
486
- response.message = "Password reset succesfull";
487
- break;
488
- }
489
- }
490
-
491
- self.wsManager.send(response);
492
- });
493
- } catch (error) {
494
- console.log("Password reset failed", error);
495
- }
496
- }
536
+ let email = {
537
+ method: "postmark.sendEmail",
538
+ host: result.host,
539
+ postmark: {
540
+ From: result.from,
541
+ To: result.email,
542
+ Subject: "Reset Your Password Easily",
543
+ HtmlBody: htmlBody,
544
+ TextBody:
545
+ "Hello, \n\nWe received a request to reset the password for your account. If you did not make this request, please ignore this email. Otherwise, you can reset your password by accessing your personal recovery link directly.",
546
+ MessageStream: "outbound"
547
+ },
548
+ organization_id: result.organization_id
549
+ };
550
+
551
+ if (server.wsManager) {
552
+ server.wsManager.emit("postmark", email);
553
+ }
554
+ break;
555
+ }
556
+ }
557
+
558
+ if (server.wsManager) {
559
+ server.wsManager.send(response);
560
+ }
561
+ } catch (error) {
562
+ console.error("Forgot Password failed", error);
563
+ }
497
564
  }
498
565
 
499
- module.exports = CoCreateUser;
566
+ /**
567
+ * Validates individual recovery tokens and writes new user credentials safely.
568
+ */
569
+ async function resetPassword(data) {
570
+ try {
571
+ if (!server || !server.crud) return;
572
+ if (!data.email || !data.password || !data.token) return;
573
+
574
+ data.method = "object.update";
575
+ data.array = "keys";
576
+ data.object = { password: data.password, recoveryId: "" };
577
+ data.$filter = {
578
+ query: { email: data.email, recoveryId: data.token },
579
+ limit: 1
580
+ };
581
+
582
+ const result = await server.crud.send(data);
583
+
584
+ let response = {
585
+ socket: result.socket,
586
+ host: result.host,
587
+ method: "resetPassword",
588
+ success: false,
589
+ message: "Token is invalid or has expired",
590
+ organization_id: result.organization_id,
591
+ uid: result.uid
592
+ };
593
+
594
+ for (let object of result.object) {
595
+ if (object._id) {
596
+ response.success = true;
597
+ response.message = "Password reset successful";
598
+ break;
599
+ }
600
+ }
601
+
602
+ if (server.wsManager) {
603
+ server.wsManager.send(response);
604
+ }
605
+ } catch (error) {
606
+ console.error("Password reset failed", error);
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Auto-initialization loop.
612
+ * Silently polls to verify that the unified server orchestrator is fully loaded,
613
+ * and that both the database (crud) and socket manager (wsManager) have completed
614
+ * their initial handshakes, then attaches active user event triggers.
615
+ */
616
+ function init() {
617
+ if (server && server.isInitialized && server.crud && server.wsManager) {
618
+ server.wsManager.on("signUp", (data) => signUp(data));
619
+ server.wsManager.on("signIn", (data) => signIn(data));
620
+ server.wsManager.on("userStatus", (data) => userStatus(data));
621
+ server.wsManager.on("checkSession", (data) => checkSession(data));
622
+ server.wsManager.on("inviteUser", (data) => inviteUser(data));
623
+ server.wsManager.on("acceptInvite", (data) => acceptInvite(data));
624
+ server.wsManager.on("forgotPassword", (data) => forgotPassword(data));
625
+ server.wsManager.on("resetPassword", (data) => resetPassword(data));
626
+ } else {
627
+ // If the shared server orchestrator is not yet ready, check again in 500ms
628
+ setTimeout(init, 500);
629
+ }
630
+ }
631
+
632
+ // Auto-execute initialization instantly upon module load
633
+ init();
634
+
635
+ export default init;