Skip to content

Commit a5d92c8

Browse files
committed
Merge branch 'main' of github.com:apostrophecms/apostrophe into radio-icon-buttons
2 parents ab52bcc + 03b1498 commit a5d92c8

5 files changed

Lines changed: 538 additions & 12 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Layout widget for dynamic grid layouts.
1111
* `widgetOperations` support for `placement: 'breadcrumb'` to add operations to the breadcrumb menu of widgets. Extend the widget operations configuration to support various features when in the breadcrumb menu.
1212
* Area template (Nunjucks) support for `aposStyle`, `aposClassName`, `aposParentOptions` and `aposAttrs` contextual named variables (`with {}` syntax).
13+
* New login option `caseInsensitive` to force login usernames and emails to be case insensitive. New task `login-case-insensitive` updating all login names / email to lowercase, used by a new migration when switching to `caseInsensitive`.
1314

1415
### Changes
1516

‎modules/@apostrophecms/login/index.js‎

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ module.exports = {
5757
username: 'apostrophe:enterUsername',
5858
password: 'apostrophe:enterPassword'
5959
},
60+
caseInsensitive: false,
6061
localLogin: true,
6162
passwordReset: false,
6263
passwordResetHours: 48,
@@ -83,6 +84,7 @@ module.exports = {
8384
self.enableBrowserData();
8485
await self.enableBearerTokens();
8586
self.addToAdminBar();
87+
self.addCaseInsensitiveMigration();
8688
},
8789
handlers(self) {
8890
return {
@@ -392,6 +394,14 @@ module.exports = {
392394
}
393395
};
394396
},
397+
tasks(self, options) {
398+
return {
399+
'case-insensitive': {
400+
usage: 'Migrate all users with case insensitive username and email',
401+
task: self.caseInsensitiveTask
402+
}
403+
};
404+
},
395405
methods(self) {
396406
return {
397407

@@ -537,13 +547,13 @@ module.exports = {
537547
// the `user` object.
538548
// `attempts`, `ip` and `requestId` are optional, sent for only logging
539549
// needs. They won't be available with passport.
540-
541550
async verifyLogin(username, password, attempts = 0, ip, requestId) {
542551
const req = self.apos.task.getReq();
552+
const loginName = self.normalizeLoginName(username);
543553
const user = await self.apos.user.find(req, {
544554
$or: [
545-
{ username },
546-
{ email: username }
555+
{ username: loginName },
556+
{ email: loginName }
547557
],
548558
disabled: { $ne: true }
549559
}).toObject();
@@ -625,6 +635,7 @@ module.exports = {
625635
// - username/email AND reset token
626636
// `resetToken` can be `false` or `string`. Passing any other type
627637
// will be converted to string and used for searching the user.
638+
// Sould we normalize here too?
628639
async getPasswordResetUser(usernameOrEmail, resetToken = false) {
629640
if (!self.isPasswordResetEnabled()) {
630641
return null;
@@ -795,10 +806,12 @@ module.exports = {
795806
// are `requirements` that require password verification occur first,
796807
// return an incomplete token.
797808
async initialLogin(req) {
798-
const username = self.apos.launder.string(req.body.username);
809+
const username = self.normalizeLoginName(
810+
self.apos.launder.string(req.body.username)
811+
);
799812
const password = self.apos.launder.string(req.body.password);
800813

801-
if (!(username && password)) {
814+
if (!username || !password) {
802815
throw self.apos.error('invalid', req.t('apostrophe:loginPageBothRequired'));
803816
}
804817

@@ -822,7 +835,7 @@ module.exports = {
822835
await self.verifyRequirements(req, onTimeRequirements);
823836

824837
// send log information
825-
const user = await self.apos.login.verifyLogin(
838+
const user = await self.verifyLogin(
826839
username,
827840
password,
828841
logAttempts,
@@ -995,6 +1008,69 @@ module.exports = {
9951008
required: true
9961009
})
9971010
);
1011+
},
1012+
1013+
normalizeLoginName(usernameOrEmail, {
1014+
caseInsensitive = self.options.caseInsensitive
1015+
} = {}) {
1016+
if (typeof usernameOrEmail !== 'string' || !caseInsensitive) {
1017+
return usernameOrEmail;
1018+
}
1019+
return usernameOrEmail.toLowerCase();
1020+
},
1021+
1022+
async addCaseInsensitiveMigration() {
1023+
if (self.options.caseInsensitive) {
1024+
self.apos.migration.add('login-case-insensitive', self.caseInsensitiveTask);
1025+
}
1026+
},
1027+
1028+
async caseInsensitiveTask() {
1029+
const duplicatedUsernames = [];
1030+
await self.apos.migration.eachDoc({ type: '@apostrophecms/user' }, 1, async (user) => {
1031+
const normalizedUsername = self.apos.login
1032+
.normalizeLoginName(user.username, { caseInsensitive: true });
1033+
const normalizedEmail = self.apos.login
1034+
.normalizeLoginName(user.email, { caseInsensitive: true });
1035+
1036+
const shouldUpdateUsername = user.username !== normalizedUsername;
1037+
const shouldUpdateEmail = user.email && user.email !== normalizedEmail;
1038+
if (!shouldUpdateUsername && !shouldUpdateEmail) {
1039+
return;
1040+
}
1041+
1042+
const criteria = {
1043+
$set: {
1044+
...shouldUpdateUsername && { username: normalizedUsername },
1045+
...shouldUpdateEmail && { email: normalizedEmail }
1046+
}
1047+
};
1048+
try {
1049+
await self.apos.user.safe.updateOne({ _id: user._id }, criteria);
1050+
await self.apos.doc.db.updateOne({ _id: user._id }, criteria);
1051+
} catch (err) {
1052+
if (self.apos.doc.isUniqueError(err)) {
1053+
duplicatedUsernames.push({
1054+
user: {
1055+
_id: user._id,
1056+
username: user.username,
1057+
email: user.email
1058+
},
1059+
conflictingFields: err.keyValue
1060+
});
1061+
return;
1062+
}
1063+
throw err;
1064+
}
1065+
});
1066+
1067+
if (duplicatedUsernames.length) {
1068+
self.logError(
1069+
'conflicting-usernames',
1070+
'Accounts with certain usernames and/or emails would be in conflict with other accounts if changed to lowercase. Please review the following usernames and emails and address them manually.',
1071+
{ failed: duplicatedUsernames }
1072+
);
1073+
}
9981074
}
9991075
};
10001076
},

‎modules/@apostrophecms/migration/index.js‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,14 @@ module.exports = {
6767
// migration, wrap them with the `await apos.global.busy(myFunction)` API.
6868
// Note that this API involves a significant startup delay to allow
6969
// existing requests to terminate.
70-
add(name, migrationFn, options) {
71-
if (!options) {
72-
options = {};
73-
}
70+
add(name, migrationFn, options = {}) {
7471
self.migrations.push({
7572
name,
7673
options,
7774
fn: migrationFn
7875
});
7976
},
77+
8078
// Invoke the iterator function once for each doc in the aposDocs
8179
// collection. If only two arguments are given, `limit` is assumed to be 1
8280
// (only one doc may be processed at a time).

‎modules/@apostrophecms/user/index.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,16 @@ module.exports = {
194194
doc.password = self.apos.util.generateId();
195195
}
196196
},
197+
normalizeLoginNames(req, doc, options) {
198+
self.normalizeUserLoginInfo(doc);
199+
},
197200
async insertSafe(req, doc, options) {
198201
return self.insertOrUpdateSafe(req, doc, 'insert');
199202
}
200203
},
201204
beforeUpdate: {
202205
async updateSafe(req, doc, options) {
206+
self.normalizeUserLoginInfo(doc);
203207
return self.insertOrUpdateSafe(req, doc, 'update');
204208
}
205209
},
@@ -230,6 +234,7 @@ module.exports = {
230234
// Reflect email and username changes in the safe after deduplicating in
231235
// the piece
232236
afterArchive: {
237+
233238
async updateSafe(req, piece) {
234239
await self.insertOrUpdateSafe(req, piece, 'update');
235240
}
@@ -238,6 +243,7 @@ module.exports = {
238243
// Reflect email and username changes in the safe after deduplicating
239244
// in the piece
240245
async updateSafe(req, piece) {
246+
self.normalizeUserLoginInfo(piece);
241247
await self.insertOrUpdateSafe(req, piece, 'update');
242248
}
243249
}
@@ -580,6 +586,14 @@ module.exports = {
580586
user.password = password;
581587
return self.update(req, user);
582588
},
589+
590+
normalizeUserLoginInfo(doc) {
591+
doc.username = self.apos.login.normalizeLoginName(doc.username);
592+
if (doc.email) {
593+
doc.email = self.apos.login.normalizeLoginName(doc.email);
594+
}
595+
},
596+
583597
...require('./lib/legacy-migrations')(self)
584598
};
585599
},

0 commit comments

Comments
 (0)