Repository: jhipster/generator-jhipster (ts)

Issue:
Login RegEx is a breaking change 
##### **Overview of the issue**

[One recent PR](https://github.com/jhipster/generator-jhipster/pull/11450) changed the validation patterns logins. This regex has a problem.

* There is a potential breaking change. E.g. with the old regex `foo@bar@com` was a valid login. With the regex in master not. So data which is currently in the database and a valid login will be invalid now

##### **Motivation for or Use Case**

It should not happen.

##### **Related issues**

None.

##### **Suggest a Fix**

Update the regex such that

* It behaves more like the old regex, such that breaking changes do not happen

##### **JHipster Version(s)**

Current master only. The was **NO** release with the breaking change, so no user is in danger.
 
##### **Browsers and Operating System**

<!-- What OS are you on? is this a problem with all browsers or only IE8? -->

-   [x] Checking this box is mandatory (this is just to show you read everything)

<!-- Love JHipster? Please consider supporting our collective:
👉  https://opencollective.com/generator-jhipster/donate -->


Pull request description:
update login regex to be more consistent with current released one
This makes the login regex more consistent with the current released version. In particular for example:

* Released version: `foo@bar@com` -> VALID :heavy_check_mark: 
* Current master: `foo@bar@com` -> INVALID :red_circle: 
* New regex: `foo@bar@com` -> VALID :heavy_check_mark: 

closes #11726

-   Please make sure the below checklist is followed for Pull Requests.

-   [ ] [All continuous integration tests](https://github.com/jhipster/generator-jhipster/actions) are green
-   [ ] Tests are added where necessary
-   [ ] Documentation is added/updated where necessary
-   [ ] Coding Rules & Commit Guidelines as per our [CONTRIBUTING.md document](https://github.com/jhipster/generator-jhipster/blob/master/CONTRIBUTING.md) are followed

<!--
Please also reference the issue number in a commit message to [automatically close the related Github issue](https://help.github.com/articles/closing-issues-via-commit-messages/)

Note: It is also possible to add `[skip ci]` or `[ci skip]` to your commit message to skip continuous integration tests
-->


Patch:
diff --git a/README.md b/README.md
index c4a3633449..3ed685e192 100644
--- a/README.md
+++ b/README.md
@@ -135,6 +135,16 @@ Support this project by becoming a sponsor! [Become a sponsor](https://opencolle
           <img width="50em" src="https://www.jhipster.tech/images/open-collective/manwoman.png">
         </a>
       </td>
+      <td align="center" valign="middle">
+        <a href="https://edubirdie.com/" target="_blank">
+          <img width="50em" src="https://www.jhipster.tech/images/open-collective/edubirdie.png">	
+        </a>
+      </td>
+      <td align="center" valign="middle">	
+        <a href="https://www.zadluzenia.com/" target="_blank">
+          <img width="50em" src="https://www.jhipster.tech/images/open-collective/zadluzenia.png">	
+        </a>
+      </td>
     </tr>
   </tbody>
 </table>
diff --git a/generators/client/index.js b/generators/client/index.js
index 356828d525..4102e3caad 100644
--- a/generators/client/index.js
+++ b/generators/client/index.js
@@ -84,6 +84,7 @@ module.exports = class extends BaseBlueprintGenerator {
 
             setupClientconsts() {
                 // Make constants available in templates
+                this.LOGIN_REGEX = constants.LOGIN_REGEX_JS;
                 this.ANGULAR = constants.SUPPORTED_CLIENT_FRAMEWORKS.ANGULAR;
                 this.HUSKY_VERSION = constants.HUSKY_VERSION;
                 this.LINT_STAGED_VERSION = constants.LINT_STAGED_VERSION;
diff --git a/generators/client/templates/angular/package.json.ejs b/generators/client/templates/angular/package.json.ejs
index 6bfeca45fa..54ad60d623 100644
--- a/generators/client/templates/angular/package.json.ejs
+++ b/generators/client/templates/angular/package.json.ejs
@@ -46,7 +46,7 @@
     "bootswatch": "4.4.1",
     <%_ } _%>
     "moment": "2.24.0",
-    "ng-jhipster": "0.12.0",
+    "ng-jhipster": "0.13.0",
     "ngx-cookie-service": "3.0.4",
     "ngx-infinite-scroll": "8.0.2",
     "ngx-webstorage": "5.0.0",
diff --git a/generators/client/templates/angular/src/main/webapp/app/account/register/register.component.ts.ejs b/generators/client/templates/angular/src/main/webapp/app/account/register/register.component.ts.ejs
index c1a6e6d7c5..8010f66ab3 100644
--- a/generators/client/templates/angular/src/main/webapp/app/account/register/register.component.ts.ejs
+++ b/generators/client/templates/angular/src/main/webapp/app/account/register/register.component.ts.ejs
@@ -42,7 +42,7 @@ export class RegisterComponent implements AfterViewInit {
     success = false;
 
     registerForm = this.fb.group({
-        login: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(50), Validators.pattern('^[a-zA-Z0-9!#$&\'*+=?^_`{|}~.-]+@?[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$')]],
+        login: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(50), Validators.pattern("<%- LOGIN_REGEX %>")]],
         email: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(254), Validators.email]],
         password: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(50)]],
         confirmPassword: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(50)]]
diff --git a/generators/client/templates/angular/src/main/webapp/app/admin/user-management/user-management-update.component.ts.ejs b/generators/client/templates/angular/src/main/webapp/app/admin/user-management/user-management-update.component.ts.ejs
index 4226d4ebc7..dacfc79d72 100644
--- a/generators/client/templates/angular/src/main/webapp/app/admin/user-management/user-management-update.component.ts.ejs
+++ b/generators/client/templates/angular/src/main/webapp/app/admin/user-management/user-management-update.component.ts.ejs
@@ -40,7 +40,7 @@ export class UserManagementUpdateComponent implements OnInit {
 
     editForm = this.fb.group({
         id: [],
-        login: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(50), Validators.pattern('^[a-zA-Z0-9!#$&\'*+=?^_`{|}~.-]+@?[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$')]],
+        login: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(50), Validators.pattern("<%- LOGIN_REGEX %>")]],
         firstName: ['', [Validators.maxLength(50)]],
         lastName: ['', [Validators.maxLength(50)]],
         email: ['', [Validators.minLength(5), Validators.maxLength(254), Validators.email]],
diff --git a/generators/client/templates/angular/src/main/webapp/app/shared/alert/alert-error.component.ts.ejs b/generators/client/templates/angular/src/main/webapp/app/shared/alert/alert-error.component.ts.ejs
index 88ceb9cb63..18d5480cdd 100644
--- a/generators/client/templates/angular/src/main/webapp/app/shared/alert/alert-error.component.ts.ejs
+++ b/generators/client/templates/angular/src/main/webapp/app/shared/alert/alert-error.component.ts.ejs
@@ -149,6 +149,7 @@ export class AlertErrorComponent implements OnDestroy {
     }
 
     close(alert: JhiAlert): void {
-        alert.close?.(this.alerts);
+        // NOSONAR can be removed after https://github.com/SonarSource/SonarJS/issues/1930 is resolved
+        alert.close?.(this.alerts); // NOSONAR
     }
 }
diff --git a/generators/client/templates/angular/src/main/webapp/app/shared/alert/alert.component.ts.ejs b/generators/client/templates/angular/src/main/webapp/app/shared/alert/alert.component.ts.ejs
index 6babce3878..39f4e435e0 100644
--- a/generators/client/templates/angular/src/main/webapp/app/shared/alert/alert.component.ts.ejs
+++ b/generators/client/templates/angular/src/main/webapp/app/shared/alert/alert.component.ts.ejs
@@ -52,6 +52,7 @@ export class AlertComponent implements OnInit, OnDestroy {
     }
 
     close(alert: JhiAlert): void {
-        alert.close?.(this.alerts);
+        // NOSONAR can be removed after https://github.com/SonarSource/SonarJS/issues/1930 is resolved
+        alert.close?.(this.alerts); // NOSONAR
     }
 }
diff --git a/generators/client/templates/react/src/main/webapp/app/modules/account/register/register.tsx.ejs b/generators/client/templates/react/src/main/webapp/app/modules/account/register/register.tsx.ejs
index 5da932ebf7..ba9df6e611 100644
--- a/generators/client/templates/react/src/main/webapp/app/modules/account/register/register.tsx.ejs
+++ b/generators/client/templates/react/src/main/webapp/app/modules/account/register/register.tsx.ejs
@@ -71,7 +71,7 @@ export const RegisterPage = (props: IRegisterProps) => {
               placeholder={translate('global.form.username.placeholder')}
               validate={{
                 required: { value: true, errorMessage: translate('register.messages.validate.login.required') },
-                pattern: { value: '^[a-zA-Z0-9!#$&\'*+=?^_`{|}~.-]+@?[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$', errorMessage: translate('register.messages.validate.login.pattern') },
+                pattern: { value: "<%- LOGIN_REGEX %>", errorMessage: translate('register.messages.validate.login.pattern') },
                 minLength: { value: 1, errorMessage: translate('register.messages.validate.login.minlength') },
                 maxLength: { value: 50, errorMessage: translate('register.messages.validate.login.maxlength') }
               }}
diff --git a/generators/client/templates/react/src/main/webapp/app/modules/administration/user-management/user-management-update.tsx.ejs b/generators/client/templates/react/src/main/webapp/app/modules/administration/user-management/user-management-update.tsx.ejs
index b14eda4da1..6a6e24a712 100644
--- a/generators/client/templates/react/src/main/webapp/app/modules/administration/user-management/user-management-update.tsx.ejs
+++ b/generators/client/templates/react/src/main/webapp/app/modules/administration/user-management/user-management-update.tsx.ejs
@@ -98,7 +98,7 @@ export const UserManagementUpdate = (props: IUserManagementUpdateProps) => {
                   errorMessage: translate('register.messages.validate.login.required')
                 },
                 pattern: {
-                  value: '^[a-zA-Z0-9!#$&\'*+=?^_`{|}~.-]+@?[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$',
+                  value: "<%- LOGIN_REGEX %>",
                   errorMessage: translate('register.messages.validate.login.pattern')
                 },
                 minLength: {
diff --git a/generators/entity-server/files.js b/generators/entity-server/files.js
index 3862f712f8..49d1c7af70 100644
--- a/generators/entity-server/files.js
+++ b/generators/entity-server/files.js
@@ -21,6 +21,7 @@ const chalk = require('chalk');
 const faker = require('faker');
 const fs = require('fs');
 const utils = require('../utils');
+const liquibaseUtils = require('../../utils/liquibase');
 const constants = require('../generator-constants');
 
 /* Use customized randexp */
@@ -33,41 +34,6 @@ const SERVER_MAIN_RES_DIR = constants.SERVER_MAIN_RES_DIR;
 const TEST_DIR = constants.TEST_DIR;
 const SERVER_TEST_SRC_DIR = constants.SERVER_TEST_SRC_DIR;
 
-/*
- * Current faker version is 4.1.0 and was release in 2017
- * It is outdated
- * https://github.com/Marak/faker.js/blob/10bfb9f467b0ac2b8912ffc15690b50ef3244f09/lib/date.js#L73-L96
- * Needed for reproducible builds
- */
-const getRecentDate = function (days, refDate) {
-    let date = new Date();
-    if (refDate !== undefined) {
-        date = new Date(Date.parse(refDate));
-    }
-
-    const range = {
-        min: 1000,
-        max: (days || 1) * 24 * 3600 * 1000,
-    };
-
-    let future = date.getTime();
-    future -= faker.random.number(range); // some time from now to N days ago, in milliseconds
-    date.setTime(future);
-
-    return date;
-};
-
-const getRecentForLiquibase = function (days, changelogDate) {
-    let formatedDate;
-    if (changelogDate !== undefined) {
-        formatedDate = `${changelogDate.substring(0, 4)}-${changelogDate.substring(4, 6)}-${changelogDate.substring(
-            6,
-            8
-        )}T${changelogDate.substring(8, 10)}:${changelogDate.substring(10, 12)}:${changelogDate.substring(12, 14)}+00:00`;
-    }
-    return getRecentDate(1, formatedDate);
-};
-
 /**
  * The default is to use a file path string. It implies use of the template method.
  * For any other config an object { file:.., method:.., template:.. } can be used
@@ -133,7 +99,7 @@ const serverFiles = {
                     options: {
                         interpolate: INTERPOLATE_REGEX,
                         context: {
-                            getRecentForLiquibase,
+                            getRecentForLiquibase: liquibaseUtils.getRecentDateForLiquibase,
                             faker,
                             randexp,
                         },
diff --git a/generators/generator-constants.js b/generators/generator-constants.js
index ae2d97ac76..1e52a9b322 100644
--- a/generators/generator-constants.js
+++ b/generators/generator-constants.js
@@ -108,6 +108,10 @@ const HELM_MOGODB_REPLICASET = '^3.10.1';
 
 // all constants used throughout all generators
 
+const LOGIN_REGEX = '^(?>[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\\\.[a-zA-Z0-9-]+)*)|(?>[_.@A-Za-z0-9-]+)$';
+// JS does not support atomic groups
+const LOGIN_REGEX_JS = '^[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\\\.[a-zA-Z0-9-]+)*$|^[_.@A-Za-z0-9-]+$';
+
 const MAIN_DIR = 'src/main/';
 const TEST_DIR = 'src/test/';
 
@@ -304,6 +308,8 @@ const constants = {
     MAIN_DIR,
     TEST_DIR,
 
+    LOGIN_REGEX,
+    LOGIN_REGEX_JS,
     // supported client frameworks
     SUPPORTED_CLIENT_FRAMEWORKS,
 
diff --git a/generators/server/index.js b/generators/server/index.js
index 68bb532a4a..7cb33d44e8 100644
--- a/generators/server/index.js
+++ b/generators/server/index.js
@@ -82,6 +82,7 @@ module.exports = class extends BaseBlueprintGenerator {
                 // Make constants available in templates
                 this.MAIN_DIR = constants.MAIN_DIR;
                 this.TEST_DIR = constants.TEST_DIR;
+                this.LOGIN_REGEX = constants.LOGIN_REGEX;
                 this.CLIENT_WEBPACK_DIR = constants.CLIENT_WEBPACK_DIR;
                 this.SERVER_MAIN_SRC_DIR = constants.SERVER_MAIN_SRC_DIR;
                 this.SERVER_MAIN_RES_DIR = constants.SERVER_MAIN_RES_DIR;
diff --git a/generators/server/templates/src/main/java/package/config/Constants.java.ejs b/generators/server/templates/src/main/java/package/config/Constants.java.ejs
index adc5e8d2e8..6bbd421e48 100644
--- a/generators/server/templates/src/main/java/package/config/Constants.java.ejs
+++ b/generators/server/templates/src/main/java/package/config/Constants.java.ejs
@@ -25,7 +25,7 @@ public final class Constants {
 
     <%_ if (!skipUserManagement || authenticationType === 'oauth2') { _%>
     // Regex for acceptable logins
-    public static final String LOGIN_REGEX = "^[a-zA-Z0-9!#$&'*+=?^_`{|}~.-]+@?[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$";
+    public static final String LOGIN_REGEX = "<%- LOGIN_REGEX %>";
 
     <%_ } _%>
     <%_ if (!skipUserManagement || authenticationType === 'oauth2' || ['sql','mongodb','couchbase'].includes(databaseType)) { _%>
diff --git a/package.json b/package.json
index de81710a0b..a1afa43aee 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
     "description": "Spring Boot + Angular/React in one handy generator",
     "files": [
         "cli",
-        "generators"
+        "generators",
+        "utils"
     ],
     "keywords": [
         "yeoman-generator",
diff --git a/utils/liquibase.js b/utils/liquibase.js
new file mode 100644
index 0000000000..d7ae0269b7
--- /dev/null
+++ b/utils/liquibase.js
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2013-2020 the original author or authors from the JHipster project.
+ *
+ * This file is part of the JHipster project, see https://www.jhipster.tech/
+ * for more information.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const faker = require('faker');
+
+module.exports = { getRecentDateForLiquibase, parseLiquibaseChangelogDate };
+
+/*
+ * Current faker version is 4.1.0 and was release in 2017
+ * It is outdated
+ * https://github.com/Marak/faker.js/blob/10bfb9f467b0ac2b8912ffc15690b50ef3244f09/lib/date.js#L73-L96
+ * Needed for reproducible builds
+ */
+function getRecentDate(days = 1, date = new Date()) {
+    const range = {
+        min: 1000,
+        max: (days || 1) * 24 * 3600 * 1000,
+    };
+
+    let future = date.getTime();
+    future -= faker.random.number(range); // some time from now to N days ago, in milliseconds
+    date.setTime(future);
+
+    return date;
+}
+
+function parseLiquibaseChangelogDate(changelogDate) {
+    if (!changelogDate || changelogDate.length !== 14) {
+        throw new Error('A valid changelogDate is required to parse.');
+    }
+    const formatedDate = `${changelogDate.substring(0, 4)}-${changelogDate.substring(4, 6)}-${changelogDate.substring(
+        6,
+        8
+    )}T${changelogDate.substring(8, 10)}:${changelogDate.substring(10, 12)}:${changelogDate.substring(12, 14)}+00:00`;
+    return new Date(Date.parse(formatedDate));
+}
+
+function getRecentDateForLiquibase(days, changelogDate) {
+    let refDate;
+    if (changelogDate) {
+        refDate = parseLiquibaseChangelogDate(changelogDate);
+    }
+    return getRecentDate(days, refDate);
+}


Test patch:
diff --git a/generators/client/templates/react/src/test/javascript/e2e/page-objects/navbar-page.ts.ejs b/generators/client/templates/react/src/test/javascript/e2e/page-objects/navbar-page.ts.ejs
index 4d746d4127..c64b237bd3 100644
--- a/generators/client/templates/react/src/test/javascript/e2e/page-objects/navbar-page.ts.ejs
+++ b/generators/client/templates/react/src/test/javascript/e2e/page-objects/navbar-page.ts.ejs
@@ -71,7 +71,7 @@ export default class NavBarPage extends BasePage {
 
   async clickOnAccountMenuItem(item: string) {
     await this.accountMenu.click();
-    await this.selector.$(`.dropdown-item[href="/account/${item}"`).click();
+    await this.selector.$(`.dropdown-item[href="/account/${item}"]`).click();
   }
 
   async clickOnAdminMenuItem(item: string) {
@@ -100,6 +100,6 @@ export default class NavBarPage extends BasePage {
 
   async autoSignOut() {
     await this.accountMenu.click();
-    await this.selector.$(`.dropdown-item[href="/logout"`).click();
+    await this.selector.$(`.dropdown-item[href="/logout"]`).click();
   }
 }
diff --git a/test/liquibase-utils.spec.js b/test/liquibase-utils.spec.js
new file mode 100644
index 0000000000..3b45510f6e
--- /dev/null
+++ b/test/liquibase-utils.spec.js
@@ -0,0 +1,55 @@
+const expect = require('chai').expect;
+const faker = require('faker');
+const assert = require('yeoman-assert');
+const liquibaseUtils = require('../utils/liquibase');
+
+describe('Liquibase Utils', () => {
+    describe('::parseLiquibaseChangelogDate', () => {
+        describe('when not passing parameters', () => {
+            it('throws', () => {
+                expect(() => liquibaseUtils.parseLiquibaseChangelogDate()).to.throw(/^A valid changelogDate is required to parse\.$/);
+            });
+        });
+        describe('when passing an invalid changelogDate', () => {
+            it('throws', () => {
+                expect(() => liquibaseUtils.parseLiquibaseChangelogDate('1234')).to.throw(/^A valid changelogDate is required to parse\.$/);
+            });
+        });
+        describe('when passing a valid changelogDate', () => {
+            it('returns a date object', () => {
+                assert(liquibaseUtils.parseLiquibaseChangelogDate('20160208210114') instanceof Date);
+            });
+            it('returns the date', () => {
+                assert.equal(liquibaseUtils.parseLiquibaseChangelogDate('20160208210114').toISOString(), '2016-02-08T21:01:14.000Z');
+            });
+        });
+    });
+    describe('::getRecentDateForLiquibase', () => {
+        beforeEach(() => {
+            faker.seed(47);
+        });
+        describe('when not passing parameters', () => {
+            it('returns a date object', () => {
+                assert(liquibaseUtils.getRecentDateForLiquibase() instanceof Date);
+            });
+        });
+        describe('when passing 1 day', () => {
+            it('returns a date object', () => {
+                assert(liquibaseUtils.getRecentDateForLiquibase(1) instanceof Date);
+            });
+        });
+        describe('when passing 1 day with changelogDate', () => {
+            it('returns a date object', () => {
+                assert(liquibaseUtils.getRecentDateForLiquibase(1, '20160208210114') instanceof Date);
+            });
+            it('returns a recent reproducible date', () => {
+                assert.equal(liquibaseUtils.getRecentDateForLiquibase(1, '20160208210114').toISOString(), '2016-02-08T18:17:47.710Z');
+            });
+        });
+        describe('when passing 10 day with changelogDate', () => {
+            it('returns a recent reproducible date', () => {
+                assert.equal(liquibaseUtils.getRecentDateForLiquibase(10, '20160208210114').toISOString(), '2016-02-07T17:46:59.078Z');
+            });
+        });
+    });
+});