@cocreate/users 1.39.4 → 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/.github/workflows/automated.yml +23 -10
- package/.github/workflows/manual.yml +1 -2
- package/CHANGELOG.md +21 -0
- package/package.json +40 -18
- package/release.config.js +12 -4
- package/src/client.js +11 -5
- package/src/server.js +595 -529
- package/src/index.js +0 -14
package/src/server.js
CHANGED
|
@@ -1,314 +1,340 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (keyCheck?.object?.length > 0) {
|
|
80
|
-
const match = keyCheck.object[0];
|
|
81
|
-
|
|
82
|
-
if (match.email === targetEmail && match.key !== targetId) {
|
|
83
|
-
response.message = "Email is already in use with another account";
|
|
84
|
-
} else if (match.key === targetId) {
|
|
85
|
-
response.message = "You already have an account, try signing in instead";
|
|
86
|
-
} else {
|
|
87
|
-
response.message = "An account with these details already exists";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
this.wsManager.send(response);
|
|
91
|
-
return; // Exit early: Gatekeeper blocked the sign-up.
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// --- STEP 2: Resolve or Create the Contact ---
|
|
95
|
-
// Since Step 1 passed, we can safely upsert the contact record.
|
|
96
|
-
if (data.user) {
|
|
97
|
-
data.user.method = "object.update";
|
|
98
|
-
data.user.host = data.host;
|
|
99
|
-
data.user.upsert = true;
|
|
100
|
-
|
|
101
|
-
if (!data.user.organization_id) {
|
|
102
|
-
data.user.organization_id = data.organization_id;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let userResult = await this.crud.send(data.user);
|
|
106
|
-
|
|
107
|
-
// --- STEP 3: Create the Key ---
|
|
108
|
-
// Uniqueness is already guaranteed by Step 1, so we just perform the creation.
|
|
109
|
-
if (data.userKey && userResult.object?.[0]?._id) {
|
|
110
|
-
const resolvedUserId = userResult.object[0]._id;
|
|
111
|
-
|
|
112
|
-
data.userKey.method = "object.create";
|
|
113
|
-
data.userKey.host = data.host;
|
|
114
|
-
data.userKey.object.key = resolvedUserId;
|
|
115
|
-
data.userKey.object.email = targetEmail;
|
|
116
|
-
|
|
117
|
-
if (!data.userKey.organization_id) {
|
|
118
|
-
data.userKey.organization_id = data.organization_id;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Get the result and check for an ID to confirm success
|
|
122
|
-
let createdKey = await this.crud.send(data.userKey);
|
|
123
|
-
|
|
124
|
-
if (createdKey.object?.[0]?._id) {
|
|
125
|
-
response.success = true;
|
|
126
|
-
response.message = "signUp successful";
|
|
127
|
-
} else {
|
|
128
|
-
response.message = "Account creation failed at credential stage";
|
|
129
|
-
}
|
|
130
|
-
} else {
|
|
131
|
-
response.message = "Account creation failed at user stage";
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
this.wsManager.send(response);
|
|
136
|
-
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.log("signup error", error);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
data = {
|
|
144
|
-
namespace: string,
|
|
145
|
-
array: string,
|
|
146
|
-
data: object,
|
|
147
|
-
eId: string,
|
|
148
|
-
key: string,
|
|
149
|
-
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.
|
|
150
79
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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 = `
|
|
312
338
|
<html>
|
|
313
339
|
<head>
|
|
314
340
|
<title>Welcome to Yellow Oracle!</title>
|
|
@@ -316,164 +342,174 @@ class CoCreateUser {
|
|
|
316
342
|
<body>
|
|
317
343
|
<p>Hello,</p>
|
|
318
344
|
|
|
319
|
-
<p>You have been invited to join ${
|
|
345
|
+
<p>You have been invited to join ${updatedData.name} on Yellow Oracle.</p>
|
|
320
346
|
|
|
321
|
-
<p><a href="${
|
|
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>
|
|
322
348
|
|
|
323
|
-
<p>Please note, this invitation link will expire in 48 hours. We encourage you to accept it soon to begin your journey with ${
|
|
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>
|
|
324
350
|
|
|
325
351
|
<p>If the button above doesn't work, you can copy and paste the following URL into your web browser:</p>
|
|
326
|
-
<p><a href="${
|
|
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>
|
|
327
353
|
|
|
328
|
-
<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@${
|
|
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>
|
|
329
355
|
|
|
330
|
-
<p>We look forward to welcoming you to ${
|
|
356
|
+
<p>We look forward to welcoming you to ${updatedData.name}'s Yellow Oracle account!</p>
|
|
331
357
|
|
|
332
358
|
</body>
|
|
333
|
-
</html>
|
|
359
|
+
</html>
|
|
334
360
|
`;
|
|
335
361
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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 = `
|
|
477
513
|
<html>
|
|
478
514
|
<head>
|
|
479
515
|
<title>Reset Your Password</title>
|
|
@@ -483,87 +519,117 @@ class CoCreateUser {
|
|
|
483
519
|
|
|
484
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>
|
|
485
521
|
|
|
486
|
-
<p><a href="${
|
|
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>
|
|
487
523
|
|
|
488
524
|
<p>This link will expire in 24 hours for your security.</p>
|
|
489
525
|
|
|
490
526
|
<p>If you're having trouble with the button above, copy and paste the URL below into your web browser:</p>
|
|
491
|
-
<p><a href="${
|
|
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>
|
|
492
528
|
|
|
493
|
-
<p>Need more help? Our support team is here for you. Contact us at <a href="mailto:support@${
|
|
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>
|
|
494
530
|
|
|
495
531
|
<p>Thank you for using our services!</p>
|
|
496
532
|
|
|
497
533
|
</body>
|
|
498
534
|
</html>
|
|
499
535
|
`;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
async resetPassword(data) {
|
|
530
|
-
const self = this;
|
|
531
|
-
try {
|
|
532
|
-
if (!data.email || !data.password || !data.token) return;
|
|
533
|
-
|
|
534
|
-
data.method = "object.update";
|
|
535
|
-
data.array = "keys";
|
|
536
|
-
data.object = { password: data.password, recoveryId: "" };
|
|
537
|
-
data.$filter = {
|
|
538
|
-
query: { email: data.email, recoveryId: data.token },
|
|
539
|
-
limit: 1
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
this.crud.send(data).then(async (data) => {
|
|
543
|
-
let response = {
|
|
544
|
-
socket: data.socket,
|
|
545
|
-
host: data.host,
|
|
546
|
-
method: "resetPassword",
|
|
547
|
-
success: false,
|
|
548
|
-
message: "Token is invalid or has expired",
|
|
549
|
-
organization_id: data.organization_id,
|
|
550
|
-
uid: data.uid
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
for (let object of data.object) {
|
|
554
|
-
if (object._id) {
|
|
555
|
-
response.success = true;
|
|
556
|
-
response.message = "Password reset succesfull";
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
self.wsManager.send(response);
|
|
562
|
-
});
|
|
563
|
-
} catch (error) {
|
|
564
|
-
console.log("Password reset failed", error);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
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
|
+
}
|
|
567
564
|
}
|
|
568
565
|
|
|
569
|
-
|
|
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;
|