Skip to content

Commit c4ff33d

Browse files
committed
ISSUE #5412 migration script working in principle
1 parent 748b333 commit c4ff33d

File tree

7 files changed

+184
-7
lines changed

7 files changed

+184
-7
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (C) 2023 3D Repo Ltd
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
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (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 <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
const migrateUsers = require('./migrateUsersToFrontegg');
19+
20+
const scripts = [
21+
{ script: migrateUsers, desc: 'migrate users and teamspaces to Frontegg' },
22+
];
23+
24+
module.exports = scripts;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright (C) 2023 3D Repo Ltd
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
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (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 <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
const { v5Path } = require('../../../interop');
19+
20+
const { getTeamspaceList } = require('../../utils');
21+
22+
const { getTeamspaceRefId, setTeamspaceRefId, getAllUsersInTeamspace } = require(`${v5Path}/models/teamspaceSettings`);
23+
const { addUserToAccount, createAccount, createUser, getAllUsersInAccount } = require(`${v5Path}/services/sso/frontegg`);
24+
25+
const { logger } = require(`${v5Path}/utils/logger`);
26+
const { updateUserId } = require(`${v5Path}/models/users`);
27+
28+
const processTeamspace = async (ts) => {
29+
let refId = await getTeamspaceRefId(ts);
30+
const isNew = !refId;
31+
if (isNew) {
32+
logger.logInfo('\t creating Frontegg account...');
33+
refId = await createAccount(ts);
34+
await setTeamspaceRefId(ts, refId);
35+
}
36+
37+
const projection = {
38+
user: 1,
39+
'customData.email': 1,
40+
'customData.userId': 1,
41+
'customData.firstName': 1,
42+
'customData.lastName': 1,
43+
};
44+
45+
const [membersKnown, membersInTs] = await Promise.all([
46+
isNew ? Promise.resolve([]) : getAllUsersInAccount(refId),
47+
getAllUsersInTeamspace(ts, projection),
48+
]);
49+
50+
const emailToUserId = {};
51+
52+
membersKnown.forEach(({ id, email }) => {
53+
emailToUserId[email] = id;
54+
});
55+
56+
await Promise.all(membersInTs.map(async ({ user, customData: { email, userId, firstName, lastName } }) => {
57+
if (emailToUserId[email]) {
58+
// This user is a registered user of the frontegg account
59+
60+
if (emailToUserId[email] !== userId) {
61+
throw new Error(`User ID mismatched for ${user}. Expected ${userId}, found ${emailToUserId}`);
62+
}
63+
} else if (!userId) {
64+
// user does not exist in frontegg, we need to create an entry
65+
logger.logInfo(`\tCreating ${user}...`);
66+
const newUserId = await createUser(refId, email, [firstName, lastName].join(' '));
67+
console.log(newUserId);
68+
if (newUserId) await updateUserId(user, newUserId);
69+
} else {
70+
logger.logInfo(`\tAdding ${user} to account...`);
71+
await addUserToAccount(refId, userId, false);
72+
}
73+
}));
74+
};
75+
76+
const run = async () => {
77+
const teamspaces = await getTeamspaceList();
78+
for (const teamspace of teamspaces) {
79+
logger.logInfo(`-${teamspace}`);
80+
// eslint-disable-next-line no-await-in-loop
81+
await processTeamspace(teamspace);
82+
}
83+
};
84+
85+
module.exports = run;

backend/src/v5/handler/db.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,9 @@ DBHandler.storeFileInGridFS = async (database, collection, filename, buffer) =>
320320
});
321321
};
322322

323-
DBHandler.createIndex = async (database, colName, indexDef, { runInBackground: background } = {}) => {
323+
DBHandler.createIndex = async (database, colName, indexDef, { runInBackground: background, unique } = {}) => {
324324
const collection = await getCollection(database, colName);
325-
const options = deleteIfUndefined({ background });
325+
const options = deleteIfUndefined({ background, unique });
326326
await collection.createIndex(indexDef, options);
327327
};
328328

backend/src/v5/models/teamspaceSettings.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ TeamspaceSetting.createTeamspaceSettings = async (teamspace, teamspaceId) => {
219219
await db.insertOne(teamspace, TEAMSPACE_SETTINGS_COL, settings);
220220
};
221221

222+
TeamspaceSetting.setTeamspaceRefId = async (teamspace, refId) => {
223+
await teamspaceSettingUpdate(teamspace, { _id: teamspace }, { $set: { refId } });
224+
};
225+
222226
TeamspaceSetting.getTeamspaceRefId = async (teamspace) => {
223227
const { refId } = await teamspaceSettingQuery(teamspace, { _id: teamspace }, { refId: 1 });
224228
return refId;
@@ -232,11 +236,11 @@ const grantPermissionToUser = async (teamspace, username, permission) => {
232236
TeamspaceSetting.grantAdminToUser = (teamspace, username) => grantPermissionToUser(teamspace,
233237
username, TEAMSPACE_ADMIN);
234238

235-
TeamspaceSetting.getAllUsersInTeamspace = async (teamspace) => {
239+
TeamspaceSetting.getAllUsersInTeamspace = async (teamspace, projection) => {
236240
const query = { 'roles.db': teamspace, 'roles.role': TEAM_MEMBER };
237-
const users = await findMany(query, { user: 1 });
241+
const users = await findMany(query, projection ?? { user: 1 });
238242

239-
return users.map(({ user }) => user);
243+
return projection ? users : users.map(({ user }) => user);
240244
};
241245

242246
TeamspaceSetting.removeUserFromAdminPrivilege = async (teamspace, user) => {

backend/src/v5/models/users.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,8 @@ User.addUser = async (newUserData) => {
161161
User.removeUser = (user) => db.deleteOne(USERS_DB_NAME, USERS_COL, { user });
162162
User.removeUsers = (users) => db.deleteMany(USERS_DB_NAME, USERS_COL, { user: { $in: users } });
163163

164+
User.ensureIndicesExist = async () => {
165+
await db.createIndex(USERS_DB_NAME, USERS_COL, { 'customData.userId': 1 }, { runInBackground: true, unique: true });
166+
};
167+
164168
module.exports = User;

backend/src/v5/services/sessions.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ const { events } = require('./eventsManager/eventsManager.constants');
2323
const expressSession = require('express-session');
2424
const { generateUUID } = require('../utils/helper/uuids');
2525
const { publish } = require('./eventsManager/eventsManager');
26+
const { ensureIndicesExist } = require('../models/users');
2627

2728
const Sessions = { };
2829
const initialiseSession = async () => {
2930
const store = await db.getSessionStore(expressSession);
31+
await ensureIndicesExist();
3032
const secure = config.public_protocol === 'https';
3133
const { secret, maxAge, domain } = config.cookie;
3234

backend/src/v5/services/sso/frontegg/index.js

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const { HEADER_TENANT_ID, META_LABEL_TEAMSPACE } = require('./frontegg.constants
1919
const { get, delete: httpDelete, post } = require('../../../utils/webRequests');
2020
const { IdentityClient } = require('@frontegg/client');
2121
const Yup = require('yup');
22+
const { deleteIfUndefined } = require('../../../utils/helper/objects');
2223
const { generateUUIDString } = require('../../../utils/helper/uuids');
2324
const { logger } = require('../../../utils/logger');
2425
const { sso: { frontegg } } = require('../../../utils/config');
@@ -123,6 +124,27 @@ Frontegg.getUserById = async (userId) => {
123124
}
124125
};
125126

127+
Frontegg.createUser = async (accountId, email, name, userData, privateUserData, bypassVerification = false) => {
128+
try {
129+
const payload = deleteIfUndefined({
130+
email,
131+
name,
132+
tenantId: accountId,
133+
metadata: userData,
134+
vendorMetadata: privateUserData,
135+
roleIds: ['APP_USER'],
136+
137+
});
138+
139+
// using the migration endpoint will automatically activate the user
140+
const url = bypassVerification ? `${config.vendorDomain}/identity/resources/migrations/v1/local` : `${config.vendorDomain}/identity/resources/vendor-only/users/v1`;
141+
const { data } = await post(url, payload, { headers: await getBearerHeader() });
142+
return data.id;
143+
} catch (err) {
144+
throw new Error(`Failed to create user(${email}) on Frontegg: ${err.message}`);
145+
}
146+
};
147+
126148
Frontegg.getTeamspaceByAccount = async (accountId) => {
127149
try {
128150
const { data: { metadata } } = await get(`${config.vendorDomain}/tenants/resources/tenants/v2/${accountId}`, await getBearerHeader());
@@ -159,12 +181,48 @@ Frontegg.createAccount = async (name) => {
159181
}
160182
};
161183

162-
Frontegg.addUserToAccount = async (accountId, userId) => {
184+
Frontegg.getAllUsersInAccount = async (accountId) => {
185+
try {
186+
const header = {
187+
...await getBearerHeader(),
188+
'frontegg-tenant-id': accountId,
189+
190+
};
191+
192+
const initialQuery = {
193+
_limit: 200,
194+
_offset: 0,
195+
_sortBy: 'email',
196+
_order: 'ASC',
197+
};
198+
199+
let query = new URLSearchParams(initialQuery).toString();
200+
const entries = [];
201+
202+
while (query?.length) {
203+
// eslint-disable-next-line no-await-in-loop
204+
const { data: { items, _links } } = await get(`${config.vendorDomain}/identity/resources/users/v3?${query}`,
205+
header);
206+
207+
items.forEach(({ id, email }) => {
208+
entries.push({ id, email });
209+
});
210+
211+
query = _links.next;
212+
}
213+
214+
return entries;
215+
} catch (err) {
216+
throw new Error(`Failed to get users from account(${accountId}) from Frontegg: ${err.message}`);
217+
}
218+
};
219+
220+
Frontegg.addUserToAccount = async (accountId, userId, sendInvite = true) => {
163221
try {
164222
const payload = {
165223
tenantId: accountId,
166224
validateTenantExist: true,
167-
skipInviteEmail: false,
225+
skipInviteEmail: !sendInvite,
168226
};
169227
await post(`${config.vendorDomain}/identity/resources/users/v1/${userId}/tenant`, payload, { headers: await getBearerHeader() });
170228
} catch (err) {

0 commit comments

Comments
 (0)