From bf11fd4c96eead1370dbe829a90da853ecd00525 Mon Sep 17 00:00:00 2001 From: SmitGala Date: Wed, 10 Apr 2024 18:05:40 +0530 Subject: [PATCH] Login UI Revamp --- app/components/auth-assets/index.ts | 9 + app/components/img-logo/index.hbs | 1 - app/components/img-logo/index.scss | 5 - .../login-component/check/index.hbs | 51 --- app/components/login-component/index.hbs | 69 ---- app/components/login-component/index.scss | 129 ------- .../login-component/login/index.hbs | 89 ----- app/components/login-component/mfa/index.hbs | 59 ---- app/components/login-component/mfa/index.js | 7 - app/components/login-component/sso/index.hbs | 82 ----- app/components/login-component/sso/index.scss | 32 -- app/components/password-recover/index.hbs | 65 ---- app/components/password-recover/index.scss | 71 ---- app/components/password-reset/index.hbs | 90 ----- .../user-login/check-type/index.hbs | 29 ++ app/components/user-login/check-type/index.ts | 25 ++ app/components/user-login/index.hbs | 82 +++++ app/components/user-login/index.scss | 12 + .../{login-component => user-login}/index.ts | 57 ++-- .../user-login/perform-mfa/index.hbs | 97 ++++++ .../user-login/perform-mfa/index.scss | 18 + .../user-login/perform-mfa/index.ts | 48 +++ .../user-login/recover-password/index.hbs | 84 +++++ .../user-login/recover-password/index.scss | 16 + .../recover-password}/index.ts | 8 +- .../user-login/reset-password/index.hbs | 112 +++++++ .../user-login/reset-password/index.scss | 26 ++ .../reset-password}/index.ts | 23 +- .../user-login/root-container/index.hbs | 16 + .../user-login/root-container/index.scss | 18 + .../user-login/root-container/index.ts | 16 + app/components/user-login/via-sso/index.hbs | 66 ++++ app/components/user-login/via-sso/index.scss | 4 + app/components/user-login/via-sso/index.ts | 32 ++ .../via-username-password/index.hbs | 98 ++++++ .../via-username-password/index.scss | 23 ++ .../user-login/via-username-password/index.ts | 37 ++ app/services/whitelabel.ts | 5 + app/styles/_component-variables.scss | 33 +- app/templates/login.hbs | 5 +- app/templates/recover.hbs | 8 +- app/templates/reset.hbs | 4 +- cypress/support/Actions/auth/LoginActions.ts | 27 +- cypress/tests/auth.spec.ts | 4 +- tests/acceptance/oidc-test.js | 33 +- .../components/password-recover-test.js | 91 ----- .../index-test.js} | 316 ++++++++++++------ .../user-login/recover-password-test.js | 136 ++++++++ translations/en.json | 23 +- translations/ja.json | 23 +- 50 files changed, 1407 insertions(+), 1007 deletions(-) create mode 100644 app/components/auth-assets/index.ts delete mode 100644 app/components/img-logo/index.scss delete mode 100644 app/components/login-component/check/index.hbs delete mode 100644 app/components/login-component/index.hbs delete mode 100644 app/components/login-component/index.scss delete mode 100644 app/components/login-component/login/index.hbs delete mode 100644 app/components/login-component/mfa/index.hbs delete mode 100644 app/components/login-component/mfa/index.js delete mode 100644 app/components/login-component/sso/index.hbs delete mode 100644 app/components/login-component/sso/index.scss delete mode 100644 app/components/password-recover/index.hbs delete mode 100644 app/components/password-recover/index.scss delete mode 100644 app/components/password-reset/index.hbs create mode 100644 app/components/user-login/check-type/index.hbs create mode 100644 app/components/user-login/check-type/index.ts create mode 100644 app/components/user-login/index.hbs create mode 100644 app/components/user-login/index.scss rename app/components/{login-component => user-login}/index.ts (84%) create mode 100644 app/components/user-login/perform-mfa/index.hbs create mode 100644 app/components/user-login/perform-mfa/index.scss create mode 100644 app/components/user-login/perform-mfa/index.ts create mode 100644 app/components/user-login/recover-password/index.hbs create mode 100644 app/components/user-login/recover-password/index.scss rename app/components/{password-recover => user-login/recover-password}/index.ts (89%) create mode 100644 app/components/user-login/reset-password/index.hbs create mode 100644 app/components/user-login/reset-password/index.scss rename app/components/{password-reset => user-login/reset-password}/index.ts (83%) create mode 100644 app/components/user-login/root-container/index.hbs create mode 100644 app/components/user-login/root-container/index.scss create mode 100644 app/components/user-login/root-container/index.ts create mode 100644 app/components/user-login/via-sso/index.hbs create mode 100644 app/components/user-login/via-sso/index.scss create mode 100644 app/components/user-login/via-sso/index.ts create mode 100644 app/components/user-login/via-username-password/index.hbs create mode 100644 app/components/user-login/via-username-password/index.scss create mode 100644 app/components/user-login/via-username-password/index.ts delete mode 100644 tests/integration/components/password-recover-test.js rename tests/integration/components/{login-component-test.js => user-login/index-test.js} (63%) create mode 100644 tests/integration/components/user-login/recover-password-test.js diff --git a/app/components/auth-assets/index.ts b/app/components/auth-assets/index.ts new file mode 100644 index 0000000000..8c5e825e90 --- /dev/null +++ b/app/components/auth-assets/index.ts @@ -0,0 +1,9 @@ +import Component from '@glimmer/component'; + +export default class AuthAssetsComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + AuthAssets: typeof AuthAssetsComponent; + } +} diff --git a/app/components/img-logo/index.hbs b/app/components/img-logo/index.hbs index 3fe218ef8f..c2b7b8aa56 100644 --- a/app/components/img-logo/index.hbs +++ b/app/components/img-logo/index.hbs @@ -3,7 +3,6 @@ {{this.whitelabel.name}} diff --git a/app/components/img-logo/index.scss b/app/components/img-logo/index.scss deleted file mode 100644 index 7de8da2ed3..0000000000 --- a/app/components/img-logo/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -.img-logo__img { - max-width: 30em; - min-width: 5em; - max-height: 30em; -} diff --git a/app/components/login-component/check/index.hbs b/app/components/login-component/check/index.hbs deleted file mode 100644 index 7ec38f651e..0000000000 --- a/app/components/login-component/check/index.hbs +++ /dev/null @@ -1,51 +0,0 @@ -
-
- - - - {{#if @showSpinner}} -
- -
- {{else}} - - {{/if}} -
-
-
\ No newline at end of file diff --git a/app/components/login-component/index.hbs b/app/components/login-component/index.hbs deleted file mode 100644 index 1670b0c531..0000000000 --- a/app/components/login-component/index.hbs +++ /dev/null @@ -1,69 +0,0 @@ -
-
- -
-

- {{t 'login'}} -

- -
- {{#if this.isCheckDone}} - {{#if this.MFAEnabled}} - - {{else}} - - {{#if this.isSSOEnabled}} - - {{else}} - - {{/if}} - - {{/if}} - {{else}} - - {{/if}} -
- {{#if this.showRegistrationLink}} -
-

{{t 'dontHaveAccount'}}

- - {{t 'register'}} - -
- {{/if}} -
-
-
\ No newline at end of file diff --git a/app/components/login-component/index.scss b/app/components/login-component/index.scss deleted file mode 100644 index a2a526ab54..0000000000 --- a/app/components/login-component/index.scss +++ /dev/null @@ -1,129 +0,0 @@ -.container { - height: 100vh; -} - -.inner-container { - width: 100%; - display: flex; - align-items: center; - flex-direction: column; - padding-top: 20vh; -} - -.login-container { - padding: 0em; - background-color: var(--white); - border-radius: 5px; - min-width: 26em; - box-shadow: 2px 2px 17px -1px rgba(0, 0, 0, 0.5); - margin-bottom: 3em; - margin-top: 3em; -} - -.login-title { - font-size: 1.72rem; - text-align: center; - margin-top: 2em; -} - -.login-footer { - margin-top: 3em; - border-top: 1px solid #efefef; - background: #f6f7f8; - padding: 1.5em 1em 1.6em; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.9em; - border-bottom-right-radius: 5px; - border-bottom-left-radius: 5px; -} - -.register-link { - margin-left: 0.5em; -} - -.action-container { - margin: 0 2em 3em 2em; -} - -.input-group { - display: flex; - align-items: stretch; - flex-grow: 1; - margin-top: 1em; -} - -.input { - align-items: center; - background-color: var(--white); - border: 1px solid #e6e6e6; - border-radius: 3px; - color: #6b6b6b; - display: inline-flex; - font-size: 14px; - height: 32px; - justify-content: flex-start; - line-height: 24px; - padding-left: 8px; - padding-right: 8px; - position: relative; - vertical-align: top; - box-shadow: inset 0 1px 2px rgba(107, 107, 107, 0.1); - max-width: 100%; - width: 100%; - &:active, - &:hover { - border: 1px solid #b3b3b3; - box-shadow: 0 0 1px grey; - } -} - -.input-connect-label { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.input-label { - border-width: 1px 0 1px 1px; - border-style: solid; - border-color: #e6e6e6; - box-shadow: inset 0 1px 2px rgba(107, 107, 107, 0.1); - padding: 0 0.6em; - display: flex; - align-items: center; - background: #f7f7f7; - border-radius: 3px 0 0 3px; -} - -.input-label :global(.input-icon) { - color: #fe4d3f; - font-size: 1.13em; - display: flex; - align-items: center; -} - -.forgot-password { - text-align: right; - font-size: 0.8em; - margin-top: 0.5em; -} - -.login-button { - margin-top: 1em; -} - -.login-button-text { - margin-left: 0.5em; -} - -.ak-button-loader { - display: inline-flex; - align-items: center; - margin-right: 0.5em; - margin-left: 0.5em; - - :global(.ak-loader-circle-indicator) { - stroke: var(--login-component-button-loader-color) !important; - } -} \ No newline at end of file diff --git a/app/components/login-component/login/index.hbs b/app/components/login-component/login/index.hbs deleted file mode 100644 index 5f521dfaad..0000000000 --- a/app/components/login-component/login/index.hbs +++ /dev/null @@ -1,89 +0,0 @@ -
-
- - -
-
- - -
-
- - {{t 'forgotPassword'}} - -
- - {{t 'login'}} - -
\ No newline at end of file diff --git a/app/components/login-component/mfa/index.hbs b/app/components/login-component/mfa/index.hbs deleted file mode 100644 index 61eb103685..0000000000 --- a/app/components/login-component/mfa/index.hbs +++ /dev/null @@ -1,59 +0,0 @@ -
- {{#if @forced}} -

- {{t 'organizationMandatory2FA'}} -

- {{/if}} - {{#if @isEmail}} -

{{t 'emailOTP'}}

-

{{t 'emailCode'}}

- {{/if}} -
- - -
- - {{t 'login'}} - -
\ No newline at end of file diff --git a/app/components/login-component/mfa/index.js b/app/components/login-component/mfa/index.js deleted file mode 100644 index a680bf2f27..0000000000 --- a/app/components/login-component/mfa/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import Component from '@glimmer/component'; - -export default class LoginMFAComponent extends Component { - get isOTPEmpty() { - return this.args.otp == ''; - } -} diff --git a/app/components/login-component/sso/index.hbs b/app/components/login-component/sso/index.hbs deleted file mode 100644 index c288e35196..0000000000 --- a/app/components/login-component/sso/index.hbs +++ /dev/null @@ -1,82 +0,0 @@ -{{#if @isEnforced}} -
-
- - -
- -
-{{else}} - -
-
- {{t 'or'}} -
-
- -{{/if}} \ No newline at end of file diff --git a/app/components/login-component/sso/index.scss b/app/components/login-component/sso/index.scss deleted file mode 100644 index ab3f173773..0000000000 --- a/app/components/login-component/sso/index.scss +++ /dev/null @@ -1,32 +0,0 @@ -.login-form-sso-separator { - height: 0.06em; - width: 100%; - background: #ebebeb; - position: relative; - margin: 3em 0em; -} - -.login-form-sso-separator-or { - text-align: center; - background: var(--white); - width: 2.3em; - position: absolute; - left: 50%; - transform: translateX(-50%) translateY(-50%); -} - -.login-form-sso-button { - width: 100%; - display: block; - margin: auto; - margin-top: 1em; - background: #ececec; - border-color: #ececec; - &:hover, - &:focus, - &:active { - color: inherit; - background: darken(#ececec, 5%); - border-color: darken(#ececec, 5%); - } -} diff --git a/app/components/password-recover/index.hbs b/app/components/password-recover/index.hbs deleted file mode 100644 index d1892674fa..0000000000 --- a/app/components/password-recover/index.hbs +++ /dev/null @@ -1,65 +0,0 @@ - - -
-
-

- {{t 'resetPassword'}} -

- - {{#if this.mailSent}} -
-
- Check your email for a link to reset your password. If it doesn't - appear within a few minutes, check your spam folder. -
-
- If you are not receiving new emails after a few attempts, please retry - after 24 hours or contact support. -
-
- {{else}} -
- {{#if this.changeset.error.username}} - - {{this.changeset.error.username.validation}} - - {{/if}} - - -
- -
- - {{t 'sendPasswordResetMail'}} - -
- -
- - {{t 'login'}}? - -
- {{/if}} -
-
\ No newline at end of file diff --git a/app/components/password-recover/index.scss b/app/components/password-recover/index.scss deleted file mode 100644 index ff9ea9707f..0000000000 --- a/app/components/password-recover/index.scss +++ /dev/null @@ -1,71 +0,0 @@ -.container { - width: 100%; - margin-top: 3em; -} - -.flex-center { - display: flex; - align-items: center; - justify-content: center; -} - -.form { - width: 25%; - min-width: 300px; - background-color: #fff; - border-radius: 5px; - overflow: hidden; - padding: 2em 1.6em; - box-sizing: border-box; -} - -.text-center { - text-align: center; -} - -.reset-success-text { - font-size: 1.1rem; -} - -.reset-success-subtext { - color: var(--black-400); - padding-top: 1rem; -} - -.send-reset-mail-btn { - border-radius: 5px; - padding-right: 10px; - padding-left: 10px; -} - -.input-wrapper { - margin: 1em 0; - - &.has-error .input-field { - border: 1px solid #fe4d3f; - } - - .input-field { - margin: 0.2em 0; - appearance: none; - background-color: #fff; - border: 1px solid #e6e6e6; - border-radius: 3px; - color: #6b6b6b; - height: 3em; - padding: 0 1em; - position: relative; - width: 100%; - max-width: 100%; - box-shadow: inset 0 1px 2px rgba(107, 107, 107, 0.1); - } -} - -.error-msg { - font-size: 0.9em; - color: #fe4d3f; -} - -.login-link { - margin: 10px 0; -} diff --git a/app/components/password-reset/index.hbs b/app/components/password-reset/index.hbs deleted file mode 100644 index 06a1a316c5..0000000000 --- a/app/components/password-reset/index.hbs +++ /dev/null @@ -1,90 +0,0 @@ - - -
-
-
- {{#if this.isVerified}} -

- {{t 'resetPassword'}} -

- -
-
- - - {{#if this.changeset.error.password}} - - {{this.changeset.error.password.validation}} - - {{/if}} -
- - -
- -
-
- - - {{#if this.changeset.error.confirm_password}} - - {{this.changeset.error.confirm_password.validation}} - - {{/if}} -
- - -
- -

- - {{t 'reset'}} - -

- -

- {{t 'login'}} ? -

- {{/if}} - - {{#if this.isNotVerified}} -
- It looks like you clicked on an invalid password reset link. -
- -
- Try again ? -
- {{/if}} -
-
-
\ No newline at end of file diff --git a/app/components/user-login/check-type/index.hbs b/app/components/user-login/check-type/index.hbs new file mode 100644 index 0000000000..46954b66f4 --- /dev/null +++ b/app/components/user-login/check-type/index.hbs @@ -0,0 +1,29 @@ +
+ + + + + {{t 'next'}} + + +
\ No newline at end of file diff --git a/app/components/user-login/check-type/index.ts b/app/components/user-login/check-type/index.ts new file mode 100644 index 0000000000..b17b5ae77b --- /dev/null +++ b/app/components/user-login/check-type/index.ts @@ -0,0 +1,25 @@ +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; + +export interface UserLoginCheckTypeComponentSignature { + Args: { + username: string; + showSpinner: boolean; + verifySSO: () => void; + }; + Element: HTMLElement; +} + +export default class UserLoginCheckTypeComponent extends Component { + handleVerification = task(async (event: SubmitEvent) => { + event.preventDefault(); + + this.args.verifySSO(); + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'UserLogin::CheckType': typeof UserLoginCheckTypeComponent; + } +} diff --git a/app/components/user-login/index.hbs b/app/components/user-login/index.hbs new file mode 100644 index 0000000000..109de13aa4 --- /dev/null +++ b/app/components/user-login/index.hbs @@ -0,0 +1,82 @@ + + + + {{t 'loginTitle'}} + + + + {{#if this.isCheckDone}} + {{#if this.MFAEnabled}} + + {{else}} + {{#if this.isSSOEnabled}} + + {{else}} + + {{/if}} + {{/if}} + {{else}} + + {{/if}} + + + + {{#if this.showRegistrationLink}} + + {{t 'dontHaveAccount'}} + + + {{t 'registerToday'}} + + + {{/if}} + \ No newline at end of file diff --git a/app/components/user-login/index.scss b/app/components/user-login/index.scss new file mode 100644 index 0000000000..d4d29ae49b --- /dev/null +++ b/app/components/user-login/index.scss @@ -0,0 +1,12 @@ +.login-input-container { + padding: 1.714em 2.857em 2.714em; + + .login-title { + font-size: 1.285rem; + padding-bottom: 1.142em; + } +} + +.login-footer { + background: var(--user-login-footer-background-color); +} diff --git a/app/components/login-component/index.ts b/app/components/user-login/index.ts similarity index 84% rename from app/components/login-component/index.ts rename to app/components/user-login/index.ts index 330208a90e..4fd2ca8dc7 100644 --- a/app/components/login-component/index.ts +++ b/app/components/user-login/index.ts @@ -13,7 +13,7 @@ import RegistrationService from 'irene/services/registration'; type OtpError = { payload: { type: string; forced: string } }; -export default class LoginComponent extends Component { +export default class UserLoginComponent extends Component { @service declare router: RouterService; @service declare intl: IntlService; @service declare session: any; @@ -28,16 +28,19 @@ export default class LoginComponent extends Component { checkToken = ''; @tracked username = ''; - password = ''; - otp = ''; + @tracked password = ''; @tracked isSSOEnabled = false; @tracked isSSOEnforced = false; @tracked MFAEnabled = false; @tracked MFAIsEmail = false; + @tracked MFAIsAuthApp = false; @tracked MFAForced = false; + @tracked showCredError = false; + @tracked showAccountLockError = false; + SSOCheckEndpoint = 'v2/sso/check'; SSOAuthenticateEndpoint = 'sso/saml2'; @@ -61,13 +64,6 @@ export default class LoginComponent extends Component { } verifySSOTask = task(async () => { - if (!this.username) { - return this.notifications.error( - this.intl.t('pleaseEnterValidEmail'), - ENV.notifications - ); - } - try { const res = await this.network.post(this.SSOCheckEndpoint, { username: this.username, @@ -109,17 +105,9 @@ export default class LoginComponent extends Component { } }); - loginTask = task(async () => { + loginTask = task(async (otp?: string) => { const username = this.username.trim(); const password = this.password.trim(); - const otp = this.otp.trim(); - - if (!username || !password) { - return this.notifications.error( - this.intl.t('pleaseEnterValidEmail'), - ENV.notifications - ); - } try { await this.session.authenticate( @@ -136,6 +124,21 @@ export default class LoginComponent extends Component { const err = error as AdapterError; if (err.payload && err.payload.message) { + if ( + err.payload.message == 'Unable to log in with provided credentials.' + ) { + this.showCredError = true; + + return; + } + + if (err.payload.message == 'Account Locked Out') { + this.showCredError = false; + this.showAccountLockError = true; + + return; + } + this.notifications.error(err.payload.message, ENV.notifications); return; @@ -155,7 +158,7 @@ export default class LoginComponent extends Component { this.logger.error(err); this.notifications.error( - this.intl.t('tPleaseEnterValidAccountDetail'), + this.intl.t('pleaseEnterValidAccountDetail'), ENV.notifications ); } @@ -191,7 +194,9 @@ export default class LoginComponent extends Component { } this.MFAEnabled = true; - this.MFAIsEmail = otpinfo.type == 'HOTP'; + this.showCredError = false; + this.MFAIsEmail = otpinfo.type === 'HOTP'; + this.MFAIsAuthApp = otpinfo.type === 'TOTP'; this.MFAForced = this.isTrue(otpinfo.forced); return true; @@ -207,7 +212,6 @@ export default class LoginComponent extends Component { reset() { this.username = ''; - this.otp = ''; this.password = ''; this.isCheckDone = false; this.checkToken = ''; @@ -216,6 +220,9 @@ export default class LoginComponent extends Component { this.MFAEnabled = false; this.MFAIsEmail = false; this.MFAForced = false; + this.MFAIsAuthApp = false; + this.showCredError = false; + this.showAccountLockError = false; } @action @@ -224,8 +231,8 @@ export default class LoginComponent extends Component { } @action - login() { - this.loginTask.perform(); + login(otp?: string) { + this.loginTask.perform(otp); } @action @@ -245,6 +252,6 @@ export default class LoginComponent extends Component { declare module '@glint/environment-ember-loose/registry' { export default interface Registry { - LoginComponent: typeof LoginComponent; + UserLogin: typeof UserLoginComponent; } } diff --git a/app/components/user-login/perform-mfa/index.hbs b/app/components/user-login/perform-mfa/index.hbs new file mode 100644 index 0000000000..26c468d91b --- /dev/null +++ b/app/components/user-login/perform-mfa/index.hbs @@ -0,0 +1,97 @@ +
+ + + {{#if @forced}} + + {{t 'organizationMandatory2FA'}} + + +
+ + {{t 'emailOTP'}} + {{/if}} + + {{#if @isEmail}} + + {{t 'emailOTP'}} + + +
+ + {{t 'emailCode'}} + {{/if}} + + {{#if @isAuthApp}} + + {{t 'authenticatorCode'}} + + {{/if}} +
+ + + + + {{#if @showAccountLockError}} + + + + + {{t 'lockedAccount'}} + + {{#if this.showLinkContactSupport}} + + {{t 'contactSupport'}} + + {{else}} + {{t 'contactSupport'}} + {{/if}} + + + {{/if}} + + {{#if @showCredError}} + + + + + {{t 'credentialsIncorrect'}} + + + {{/if}} + + + {{#if @showAccountLockError}} + + {{t 'resetPassword'}} + + {{else}} + + {{t 'verify'}} + + {{/if}} +
+
\ No newline at end of file diff --git a/app/components/user-login/perform-mfa/index.scss b/app/components/user-login/perform-mfa/index.scss new file mode 100644 index 0000000000..816697510b --- /dev/null +++ b/app/components/user-login/perform-mfa/index.scss @@ -0,0 +1,18 @@ +.error-stack { + margin-top: 0.285em; + + .error-icon { + padding-top: 0.142em; + font-size: 1rem !important; + } + + .error-message { + padding-left: 0.714em; + font-size: 0.857rem; + } +} + +.contact-support-link { + color: var(--user-login-perform-mfa-contact-support-color); + text-decoration: underline; +} diff --git a/app/components/user-login/perform-mfa/index.ts b/app/components/user-login/perform-mfa/index.ts new file mode 100644 index 0000000000..36b45a8b81 --- /dev/null +++ b/app/components/user-login/perform-mfa/index.ts @@ -0,0 +1,48 @@ +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; +import IntlService from 'ember-intl/services/intl'; +import { task } from 'ember-concurrency'; +import { tracked } from 'tracked-built-ins'; +import WhitelabelService from 'irene/services/whitelabel'; + +export interface UserLoginPerformMFAComponentSignature { + Args: { + forced: boolean; + isEmail: boolean; + isAuthApp: boolean; + showSpinner: boolean; + login: (otp: string) => void; + showCredError: boolean; + showAccountLockError: boolean; + }; + Element: HTMLElement; +} + +export default class UserLoginPerformMFAComponent extends Component { + @service declare intl: IntlService; + @service declare whitelabel: WhitelabelService; + + @tracked otp = ''; + + get mfaInputLabel() { + return this.args.isAuthApp + ? this.intl.t('authenticatorCodeLabel') + : this.intl.t('emailCodeLabel'); + } + + handleLogin = task(async (event: SubmitEvent) => { + event.preventDefault(); + + this.args.login(this.otp); + }); + + get showLinkContactSupport() { + return this.whitelabel.show_contact_support; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'UserLogin::PerformMfa': typeof UserLoginPerformMFAComponent; + } +} diff --git a/app/components/user-login/recover-password/index.hbs b/app/components/user-login/recover-password/index.hbs new file mode 100644 index 0000000000..8833e68261 --- /dev/null +++ b/app/components/user-login/recover-password/index.hbs @@ -0,0 +1,84 @@ + + + + {{t 'resetPasswordLabel'}} + + + {{#if this.mailSent}} + + + {{t 'resetPasswordMessageToCheck'}} + + + + {{t 'resetPasswordMessageToRetry'}} + + + + {{else}} +
+ + + + + {{t 'resetPassword'}} + + +
+ {{/if}} +
+ + {{#unless this.mailSent}} + + {{t 'takeMeTo'}} + + + {{t 'login'}} + + + {{t 'page'}} + + {{/unless}} +
\ No newline at end of file diff --git a/app/components/user-login/recover-password/index.scss b/app/components/user-login/recover-password/index.scss new file mode 100644 index 0000000000..73604cd166 --- /dev/null +++ b/app/components/user-login/recover-password/index.scss @@ -0,0 +1,16 @@ +.recover-password-input-container { + padding: 1.714em 2.857em 2.714em; + + .recover-password-title { + padding-bottom: 1.142em; + font-size: 1.285rem; + + .retry-text { + color: var(--user-login-recover-password-retry-text-color); + } + } +} + +.recover-password-footer { + background: var(--user-login-recover-password-footer-background-color); +} diff --git a/app/components/password-recover/index.ts b/app/components/user-login/recover-password/index.ts similarity index 89% rename from app/components/password-recover/index.ts rename to app/components/user-login/recover-password/index.ts index 55dfb06cbb..32eea513c6 100644 --- a/app/components/password-recover/index.ts +++ b/app/components/user-login/recover-password/index.ts @@ -20,7 +20,7 @@ type ChangesetBufferProps = BufferedChangeset & { username: string; }; -export default class PasswordRecoverComponent extends Component { +export default class UserLoginRecoverPasswordComponent extends Component { @service declare ajax: any; @service('notifications') declare notify: NotificationService; @service('rollbar') declare logger: LoggerService; @@ -84,3 +84,9 @@ export default class PasswordRecoverComponent extends Component { } }); } + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'UserLogin::RecoverPassword': typeof UserLoginRecoverPasswordComponent; + } +} diff --git a/app/components/user-login/reset-password/index.hbs b/app/components/user-login/reset-password/index.hbs new file mode 100644 index 0000000000..054c56fbeb --- /dev/null +++ b/app/components/user-login/reset-password/index.hbs @@ -0,0 +1,112 @@ + + + + {{t 'resetPasswordLabel'}} + + + {{#if this.isVerified}} +
+ + + + + {{#if this.changeset.error.password}} + + + + + {{this.changeset.error.password.validation}} + + + {{/if}} + + + + + + {{#if this.changeset.error.confirm_password}} + + + + + {{this.changeset.error.confirm_password.validation}} + + + {{/if}} + + + + {{t 'reset'}} + + +
+ {{/if}} + + {{#if this.isNotVerified}} + + {{t 'invalidPasswordResetLink'}} + + {{/if}} +
+ + + {{t 'takeMeTo'}} + + + {{t 'login'}} + + + {{t 'page'}} + +
\ No newline at end of file diff --git a/app/components/user-login/reset-password/index.scss b/app/components/user-login/reset-password/index.scss new file mode 100644 index 0000000000..d17a71546c --- /dev/null +++ b/app/components/user-login/reset-password/index.scss @@ -0,0 +1,26 @@ +.reset-password-input-container { + padding: 1.714em 2.857em 2.714em; + + .reset-password-title { + padding-bottom: 1.142em; + font-size: 1.285rem; + } +} + +.reset-password-footer { + background: var(--user-login-reset-password-footer-background-color); +} + +.error-stack { + margin-top: 0.285em; + + .error-icon { + padding-top: 0.142em; + font-size: 1rem !important; + } + + .error-message { + padding-left: 0.714em; + font-size: 0.857rem; + } +} diff --git a/app/components/password-reset/index.ts b/app/components/user-login/reset-password/index.ts similarity index 83% rename from app/components/password-reset/index.ts rename to app/components/user-login/reset-password/index.ts index dd1c81f35a..72a03a5e4b 100644 --- a/app/components/password-reset/index.ts +++ b/app/components/user-login/reset-password/index.ts @@ -16,7 +16,7 @@ import { import IntlService from 'ember-intl/services/intl'; import RouterService from '@ember/routing/router-service'; -interface PasswordResetSignature { +interface UserLoginResetPasswordComponentSignature { Args: { token?: string; }; @@ -32,7 +32,7 @@ const ResetValidator = { confirm_password: validateConfirmation({ on: 'password' }), }; -export default class PasswordResetComponent extends Component { +export default class UserLoginResetPasswordComponent extends Component { @service declare intl: IntlService; @service declare ajax: any; @service declare router: RouterService; @@ -47,7 +47,10 @@ export default class PasswordResetComponent extends Component + + + + + + + + {{yield}} + + \ No newline at end of file diff --git a/app/components/user-login/root-container/index.scss b/app/components/user-login/root-container/index.scss new file mode 100644 index 0000000000..82a28285bb --- /dev/null +++ b/app/components/user-login/root-container/index.scss @@ -0,0 +1,18 @@ +.login-root { + height: 100vh; + + .root-container { + border: 1px solid var(--user-login-root-container-border-color); + border-radius: var(--user-login-root-container-border-radius); + min-width: 29.5em; + max-width: 29.5em; + box-shadow: var(--user-login-root-container-box-shadow); + + .logo-container { + img { + max-width: 12em; + max-height: 7em; + } + } + } +} diff --git a/app/components/user-login/root-container/index.ts b/app/components/user-login/root-container/index.ts new file mode 100644 index 0000000000..e9103c5c19 --- /dev/null +++ b/app/components/user-login/root-container/index.ts @@ -0,0 +1,16 @@ +import Component from '@glimmer/component'; + +export interface UserLoginRootContainerComponentSignature { + Element: HTMLElement; + Blocks: { + default: []; + }; +} + +export default class UserLoginRootContainerComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'UserLogin::RootContainer': typeof UserLoginRootContainerComponent; + } +} diff --git a/app/components/user-login/via-sso/index.hbs b/app/components/user-login/via-sso/index.hbs new file mode 100644 index 0000000000..3773ed688d --- /dev/null +++ b/app/components/user-login/via-sso/index.hbs @@ -0,0 +1,66 @@ +{{#if @isEnforced}} +
+ + + + + {{t 'ssoLogin'}} + + +
+{{else}} + + + + + + {{t 'or'}} + + + + + + {{t 'ssoLogin'}} + +{{/if}} \ No newline at end of file diff --git a/app/components/user-login/via-sso/index.scss b/app/components/user-login/via-sso/index.scss new file mode 100644 index 0000000000..05b1417b47 --- /dev/null +++ b/app/components/user-login/via-sso/index.scss @@ -0,0 +1,4 @@ +.login-form-sso-separator { + width: 10.8em; + margin: 1em 0em; +} diff --git a/app/components/user-login/via-sso/index.ts b/app/components/user-login/via-sso/index.ts new file mode 100644 index 0000000000..633a2825f5 --- /dev/null +++ b/app/components/user-login/via-sso/index.ts @@ -0,0 +1,32 @@ +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; + +export interface UserLoginViaSsoComponentSignature { + Args: { + username: string; + password: string; + usernameChanged: (event: Event) => void; + showLoginSpinner: boolean; + login: () => void; + showSSOSpinner: boolean; + ssologin: () => void; + isEnforced: boolean; + showCredError: boolean; + showAccountLockError: boolean; + }; + Element: HTMLElement; +} + +export default class UserLoginViaSsoComponent extends Component { + handleSsoLogin = task(async (event: SubmitEvent) => { + event.preventDefault(); + + this.args.ssologin(); + }); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'UserLogin::ViaSso': typeof UserLoginViaSsoComponent; + } +} diff --git a/app/components/user-login/via-username-password/index.hbs b/app/components/user-login/via-username-password/index.hbs new file mode 100644 index 0000000000..8a3f254a2e --- /dev/null +++ b/app/components/user-login/via-username-password/index.hbs @@ -0,0 +1,98 @@ +
+ + + + + + + {{t 'password'}} + + + + {{t 'forgotPassword'}} + + + + + + {{#if @showAccountLockError}} + + + + + {{t 'lockedAccount'}} + + {{#if this.showLinkContactSupport}} + + {{t 'contactSupport'}} + + {{else}} + {{t 'contactSupport'}} + {{/if}} + + + {{/if}} + + {{#if @showCredError}} + + + + + {{t 'credentialsIncorrect'}} + + + {{/if}} + + + {{#if @showAccountLockError}} + + {{t 'resetPassword'}} + + {{else}} + + {{t 'login'}} + + {{/if}} + +
\ No newline at end of file diff --git a/app/components/user-login/via-username-password/index.scss b/app/components/user-login/via-username-password/index.scss new file mode 100644 index 0000000000..5ab2fa8245 --- /dev/null +++ b/app/components/user-login/via-username-password/index.scss @@ -0,0 +1,23 @@ +.forgot-password-link { + color: var(--user-login-via-username-password-link-color); + margin-bottom: 0.35em; +} + +.error-stack { + margin-top: 0.285em; + + .error-icon { + padding-top: 0.142em; + font-size: 1rem !important; + } + + .error-message { + padding-left: 0.714em; + font-size: 0.857rem; + } +} + +.contact-support-link { + color: var(--user-login-via-username-password-contact-support-color); + text-decoration: underline; +} diff --git a/app/components/user-login/via-username-password/index.ts b/app/components/user-login/via-username-password/index.ts new file mode 100644 index 0000000000..22bb9775eb --- /dev/null +++ b/app/components/user-login/via-username-password/index.ts @@ -0,0 +1,37 @@ +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; +import WhitelabelService from 'irene/services/whitelabel'; + +export interface UserLoginViaUsernamePasswordComponentSignature { + Args: { + username: string; + password: string; + usernameChanged: (event: Event) => void; + login: () => void; + showSpinner: boolean; + showCredError: boolean; + showAccountLockError: boolean; + }; + Element: HTMLElement; +} + +export default class UserLoginViaUsernamePasswordComponent extends Component { + @service declare whitelabel: WhitelabelService; + + handleLogin = task(async (event: SubmitEvent) => { + event.preventDefault(); + + this.args.login(); + }); + + get showLinkContactSupport() { + return this.whitelabel.show_contact_support; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'UserLogin::ViaUsernamePassword': typeof UserLoginViaUsernamePasswordComponent; + } +} diff --git a/app/services/whitelabel.ts b/app/services/whitelabel.ts index 1d666a1500..60a719b39a 100644 --- a/app/services/whitelabel.ts +++ b/app/services/whitelabel.ts @@ -6,6 +6,7 @@ import ConfigurationService from './configuration'; export default class WhitelabelService extends Service { @service declare configuration: ConfigurationService; + @service('browser/window') declare window: Window; default_name = 'Appknox'; default_theme = 'dark'; @@ -68,4 +69,8 @@ export default class WhitelabelService extends Service { return this.configuration.imageData.logo_on_darkbg; } + + get show_contact_support() { + return this.window.location.href.includes('secure.appknox.com'); + } } diff --git a/app/styles/_component-variables.scss b/app/styles/_component-variables.scss index 593b9f919f..8c645f03ca 100644 --- a/app/styles/_component-variables.scss +++ b/app/styles/_component-variables.scss @@ -823,8 +823,37 @@ body { --border-color-2 ); - // variables for login-component - --login-component-button-loader-color: var(--primary-contrast-text); + // variables for user-login + --user-login-footer-background-color: var(--neutral-grey-100); + + // variables for user-login/via-username-password + --user-login-via-username-password-link-color: var(--neutral-grey-700); + --user-login-via-username-password-contact-support-color: var( + --secondary-main + ); + + // variables for user-login/perform-mfa + --user-login-via-username-password-link-color: var(--neutral-grey-700); + --user-login-perform-mfa-contact-support-color: var(--secondary-main); + + // variables for user-login/via-sso + --user-login-via-sso-separator-color: var(--neutral-grey-300); + --user-login-via-sso-separator-or-bg-color: var(--white); + --user-login-via-sso-separator-or-color: var(--background-dark); + + // variables for user-login/recover-password + --user-login-recover-password-footer-background-color: var( + --neutral-grey-100 + ); + --user-login-recover-password-retry-text-color: var(--neutral-grey-700); + + // variables for user-login/reset-password + --user-login-reset-password-footer-background-color: var(--neutral-grey-100); + + // variables for user-login/root-container + --user-login-root-container-border-color: var(--neutral-grey-200); + --user-login-root-container-box-shadow: var(--box-shadow-3); + --user-login-root-container-border-radius: var(--border-radius); // variables for file-chart --file-chart-severity-level-color-critical: var(--severity-critical); diff --git a/app/templates/login.hbs b/app/templates/login.hbs index 1a212e0cbd..686fae6cc2 100644 --- a/app/templates/login.hbs +++ b/app/templates/login.hbs @@ -1,2 +1,3 @@ -{{page-title "Login"}} - +{{page-title 'Login'}} + + \ No newline at end of file diff --git a/app/templates/recover.hbs b/app/templates/recover.hbs index 840f2c0a39..0fc29f6f11 100644 --- a/app/templates/recover.hbs +++ b/app/templates/recover.hbs @@ -1,5 +1,5 @@ -{{page-title "Recover Password"}} +{{page-title 'Recover Password'}} -
- -
+
+ +
\ No newline at end of file diff --git a/app/templates/reset.hbs b/app/templates/reset.hbs index c3ca1d1ff0..5c6829b055 100644 --- a/app/templates/reset.hbs +++ b/app/templates/reset.hbs @@ -1,5 +1,5 @@ {{page-title 'Reset Password'}} -
- +
+
\ No newline at end of file diff --git a/cypress/support/Actions/auth/LoginActions.ts b/cypress/support/Actions/auth/LoginActions.ts index e903c0441d..f2778975a3 100644 --- a/cypress/support/Actions/auth/LoginActions.ts +++ b/cypress/support/Actions/auth/LoginActions.ts @@ -32,12 +32,15 @@ export default class LoginActions { // Wait for frontend config request to resolve cy.wait('@frontendConfig'); - cy.findByPlaceholderText('Username / Email').type(username); // Username/Email field - cy.findByLabelText('login-user-check-icon').click(); + cy.findByPlaceholderText( + cyTranslate('usernameEmailIdTextPlaceholder') + ).type(username); // Username/Email field + + cy.findByLabelText('login-next-button').click(); cy.wait('@checkUserRoute', { timeout: 30000 }); - cy.findByPlaceholderText('Password').type(password); // Password field + cy.findByPlaceholderText(cyTranslate('passwordPlaceholder')).type(password); // Password field cy.findByLabelText('login-submit-button').click(); cy.wait('@loginAPIReq', { timeout: 30000 }); @@ -65,14 +68,15 @@ export default class LoginActions { if (res.registration_link) { cy.findByText(cyTranslate('dontHaveAccount')).should('exist'); - cy.findByText(cyTranslate('register')) - .should('exist') - .should('have.attr', 'href', res.registration_link); + cy.findByText(cyTranslate('registerToday')).should('exist'); } }); - cy.findByPlaceholderText('Username / Email').should('be.visible'); // Username/Email field - cy.findAllByLabelText('login-user-check-icon').should('exist'); // User check button; + cy.findByPlaceholderText( + cyTranslate('usernameEmailIdTextPlaceholder') + ).should('be.visible'); // Username/Email field + + cy.findByLabelText('login-next-button'); // User check button; } /** @@ -89,8 +93,11 @@ export default class LoginActions { cacheAcrossSpecs, validate: () => { // If these elements are displayed, token is expired - cy.findByPlaceholderText('Username / Email').should('not.exist'); // Username/Email field - cy.findByLabelText('login-user-check-icon').should('not.exist'); // User check button;s + cy.findByPlaceholderText( + cyTranslate('usernameEmailIdTextPlaceholder') + ).should('not.exist'); // Username/Email field + + cy.findByLabelText('login-next-button').should('not.exist'); // User check buttons // Validate presence of access token in localStorage. cy.window() diff --git a/cypress/tests/auth.spec.ts b/cypress/tests/auth.spec.ts index 83fc7b617c..cfe3635a39 100644 --- a/cypress/tests/auth.spec.ts +++ b/cypress/tests/auth.spec.ts @@ -32,14 +32,14 @@ describe('User Login', () => { cy.url().should('contain', APPLICATION_ROUTES.login); // Login page title - cy.findByText(cyTranslate('login')).should('exist'); + cy.findByText(cyTranslate('loginTitle')).should('exist'); // Programmatically checks for login page elements loginActions.checkLoginPageElements(frontendConfigAlias); }); it('should throw an error if user credentials are invalid', () => { - const errorMessage = 'Unable to log in with provided credentials.'; + const errorMessage = 'The credentials you entered is incorrect'; // Mocks Frontend and Server configs const { frontendConfigAlias } = diff --git a/tests/acceptance/oidc-test.js b/tests/acceptance/oidc-test.js index 1c40d980f5..898502534b 100644 --- a/tests/acceptance/oidc-test.js +++ b/tests/acceptance/oidc-test.js @@ -172,32 +172,45 @@ module('Acceptance | oidc login', function (hooks) { ) ); - await fillIn('[data-test-login-check-username-input]', 'appknoxusername'); + await fillIn( + '[data-test-user-login-check-type-username-input]', + 'appknoxusername' + ); - await click('[data-test-login-check-button]'); + await click('[data-test-user-login-check-type-button]'); if (sso) { assert - .dom('[data-test-login-sso-forced-username-input]') + .dom('[data-test-user-login-via-sso-forced-username-input]') .hasValue('appknoxusername'); - assert.dom('[data-test-login-sso-forced-button]').isNotDisabled(); + assert + .dom('[data-test-user-login-via-sso-forced-button]') + .isNotDisabled(); - await click('[data-test-login-sso-forced-button]'); + await click('[data-test-user-login-via-sso-forced-button]'); // simulate server redirect await visit(`/saml2/redirect?sso_token=${SSO_TOKEN}`); } else { assert - .dom('[data-test-login-login-username-input]') + .dom('[data-test-user-login-via-username-password-username-input]') .hasValue('appknoxusername'); - assert.dom('[data-test-login-login-password-input]').exists(); - assert.dom('[data-test-login-login-button]').isNotDisabled(); + assert + .dom('[data-test-user-login-via-username-password-password-input]') + .exists(); + + await fillIn( + '[data-test-user-login-via-username-password-password-input]', + 'appknoxpassword' + ); - await fillIn('[data-test-login-login-password-input]', 'appknoxpassword'); + assert + .dom('[data-test-user-login-via-username-password-login-button]') + .isNotDisabled(); - await click('[data-test-login-login-button]'); + await click('[data-test-user-login-via-username-password-login-button]'); } }; diff --git a/tests/integration/components/password-recover-test.js b/tests/integration/components/password-recover-test.js deleted file mode 100644 index eafa9e0da6..0000000000 --- a/tests/integration/components/password-recover-test.js +++ /dev/null @@ -1,91 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click, fillIn } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { setupIntl } from 'ember-intl/test-support'; - -import Service from '@ember/service'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { Response } from 'miragejs'; - -class LoggerStub extends Service {} - -class NotificationStub extends Service {} - -module('Integration | Component | password-recover', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - setupIntl(hooks); - - hooks.beforeEach(async function () { - this.owner.register('service:rollbar', LoggerStub); - this.owner.register('service:notifications', NotificationStub); - }); - - test('it renders password recover page', async function (assert) { - await render(hbs``); - - assert.dom('[data-test-username-email-input]').exists(); - assert.dom('[data-test-send-reset-mail-btn]').exists(); - - assert - .dom('[data-test-reset-password-header-text]') - .exists() - .containsText('t:resetPassword:()'); - - assert - .dom('[data-test-reset-password-login-link]') - .exists() - .containsText('t:login:()') - .hasAttribute('href', /login$/); - }); - - test('should show error for empty username/email', async function (assert) { - await render(hbs``); - - assert.dom('[data-test-username-email-input]').exists().hasNoValue(); - assert.dom('[data-test-username-error]').doesNotExist(); - - await click('[data-test-send-reset-mail-btn]'); - - assert.dom('[data-test-username-error]').exists(); - }); - - test('should show message for password recovery mail sent', async function (assert) { - assert.expect(6); - - const username = 'appknox'; - - this.server.post('/v2/forgot_password', (schema, req) => { - const reqUsername = req.requestBody.split('=')[1]; - - assert.strictEqual(username, reqUsername); - - return new Response(200); - }); - - await render(hbs``); - - assert.dom('[data-test-username-email-input]').exists(); - - const usernameEmailInput = this.element.querySelector( - '[data-test-username-email-input]' - ); - - await fillIn(usernameEmailInput, username); - - assert.dom('[data-test-username-email-input]').hasValue(username); - - await click('[data-test-send-reset-mail-btn]'); - - assert - .dom('[data-test-mail-sent-text]') - .exists() - .containsText( - "Check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder." - ) - .containsText( - 'If you are not receiving new emails after a few attempts, please retry after 24 hours or contact support.' - ); - }); -}); diff --git a/tests/integration/components/login-component-test.js b/tests/integration/components/user-login/index-test.js similarity index 63% rename from tests/integration/components/login-component-test.js rename to tests/integration/components/user-login/index-test.js index 3cd67f864a..68f5731463 100644 --- a/tests/integration/components/login-component-test.js +++ b/tests/integration/components/user-login/index-test.js @@ -34,7 +34,7 @@ class SessionStub extends Service { authenticateTestHook() {} } -module('Integration | Component | login component', function (hooks) { +module('Integration | Component | user-login', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); setupIntl(hooks); @@ -44,21 +44,23 @@ module('Integration | Component | login component', function (hooks) { }); test('it renders', async function (assert) { - await render(hbs``); - assert.dom('[data-test-login-check]').exists(); + await render(hbs``); + assert.dom('[data-test-user-login-check-type]').exists(); }); test('it should render check form by default', async function (assert) { - await render(hbs``); - assert.dom('[data-test-login-check]').exists(); - assert.dom('[data-test-login-check-username-input]').exists(); - assert.dom('[data-test-login-check-button]').exists(); + await render(hbs``); + + assert.dom('[data-test-user-login-check-type]').exists(); + assert.dom('[data-test-user-login-check-type-username-input]').exists(); + assert.dom('[data-test-user-login-check-type-button]').exists(); }); test('it should render login form from check for not sso', async function (assert) { this.server.post('v2/sso/check', function (schema, request) { const body = JSON.parse(request.requestBody); assert.equal(body.username, 'appknoxusername'); + return new Response( 200, {}, @@ -69,25 +71,37 @@ module('Integration | Component | login component', function (hooks) { } ); }); - await render(hbs``); - assert.dom('[data-test-login-check]').exists(); - assert.dom('[data-test-login-check-username-input]').exists(); - assert.dom('[data-test-login-check-button]').exists(); + await render(hbs``); + + assert.dom('[data-test-user-login-check-type]').exists(); + assert.dom('[data-test-user-login-check-type-username-input]').exists(); + assert.dom('[data-test-user-login-check-type-button]').exists(); const usernameInput = this.element.querySelector( - '[data-test-login-check-username-input]' + '[data-test-user-login-check-type-username-input]' ); const arrowbtn = this.element.querySelector( - '[data-test-login-check-button]' + '[data-test-user-login-check-type-button]' ); + await fillIn(usernameInput, 'appknoxusername'); await click(arrowbtn); - assert.dom('[data-test-login-login]').exists(); - assert.dom('[data-test-login-login-username-input]').exists(); - assert.dom('[data-test-login-login-password-input]').exists(); - assert.dom('[data-test-login-login-button]').exists(); + + assert.dom('[data-test-user-login-via-username-password]').exists(); + assert + .dom('[data-test-user-login-via-username-password-username-input]') + .exists(); + + assert + .dom('[data-test-user-login-via-username-password-password-input]') + .exists(); + assert - .dom('[data-test-login-login-username-input]') + .dom('[data-test-user-login-via-username-password-login-button]') + .exists(); + + assert + .dom('[data-test-user-login-via-username-password-username-input]') .hasValue('appknoxusername'); }); @@ -95,6 +109,7 @@ module('Integration | Component | login component', function (hooks) { this.server.post('v2/sso/check', function (schema, request) { const body = JSON.parse(request.requestBody); assert.equal(body.username, 'appknoxusername'); + return new Response( 200, {}, @@ -105,40 +120,64 @@ module('Integration | Component | login component', function (hooks) { } ); }); - await render(hbs``); - assert.dom('[data-test-login-check]').exists(); - assert.dom('[data-test-login-check-username-input]').exists(); - assert.dom('[data-test-login-check-button]').exists(); + + await render(hbs``); + + assert.dom('[data-test-user-login-check-type]').exists(); + assert.dom('[data-test-user-login-check-type-username-input]').exists(); + assert.dom('[data-test-user-login-check-type-button]').exists(); const usernameInput = this.element.querySelector( - '[data-test-login-check-username-input]' + '[data-test-user-login-check-type-username-input]' ); const arrowbtn = this.element.querySelector( - '[data-test-login-check-button]' + '[data-test-user-login-check-type-button]' ); + await fillIn(usernameInput, 'appknoxusername'); await click(arrowbtn); - assert.dom('[data-test-login-login]').exists(); - assert.dom('[data-test-login-login-username-input]').exists(); - assert.dom('[data-test-login-login-password-input]').exists(); - assert.dom('[data-test-login-login-button]').exists(); + + assert.dom('[data-test-user-login-via-username-password]').exists(); + assert + .dom('[data-test-user-login-via-username-password-username-input]') + .exists(); + assert - .dom('[data-test-login-login-username-input]') + .dom('[data-test-user-login-via-username-password-password-input]') + .exists(); + + assert + .dom('[data-test-user-login-via-username-password-login-button]') + .exists(); + + assert + .dom('[data-test-user-login-via-username-password-username-input]') .hasValue('appknoxusername'); const usernameLoginInput = this.element.querySelector( - '[data-test-login-login-username-input]' + '[data-test-user-login-via-username-password-username-input]' ); + await fillIn(usernameLoginInput, 'appknoxuser'); - assert.dom('[data-test-login-login]').doesNotExist(); - assert.dom('[data-test-login-login-username-input]').doesNotExist(); - assert.dom('[data-test-login-login-password-input]').doesNotExist(); - assert.dom('[data-test-login-login-button]').doesNotExist(); - assert.dom('[data-test-login-check]').exists(); - assert.dom('[data-test-login-check-username-input]').exists(); - assert.dom('[data-test-login-check-button]').exists(); + + assert.dom('[data-test-user-login-via-username-password]').doesNotExist(); assert - .dom('[data-test-login-check-username-input]') + .dom('[data-test-user-login-via-username-password-username-input]') + .doesNotExist(); + + assert + .dom('[data-test-user-login-via-username-password-password-input]') + .doesNotExist(); + + assert + .dom('[data-test-user-login-via-username-password-login-button]') + .doesNotExist(); + + assert.dom('[data-test-user-login-check-type]').exists(); + assert.dom('[data-test-user-login-check-type-username-input]').exists(); + assert.dom('[data-test-user-login-check-type-button]').exists(); + assert + .dom('[data-test-user-login-check-type-username-input]') .hasValue('appknoxuser'); }); @@ -146,6 +185,7 @@ module('Integration | Component | login component', function (hooks) { this.server.post('v2/sso/check', function (schema, request) { const body = JSON.parse(request.requestBody); assert.equal(body.username, 'appknoxusername'); + return new Response( 200, {}, @@ -156,31 +196,48 @@ module('Integration | Component | login component', function (hooks) { } ); }); - await render(hbs``); + + await render(hbs``); + const usernameInput = this.element.querySelector( - '[data-test-login-check-username-input]' + '[data-test-user-login-check-type-username-input]' ); const arrowbtn = this.element.querySelector( - '[data-test-login-check-button]' + '[data-test-user-login-check-type-button]' ); + await fillIn(usernameInput, 'appknoxusername'); await click(arrowbtn); - assert.dom('[data-test-login-login]').exists(); - assert.dom('[data-test-login-login-username-input]').exists(); - assert.dom('[data-test-login-login-password-input]').exists(); - assert.dom('[data-test-login-login-button]').exists(); + + assert.dom('[data-test-user-login-via-username-password]').exists(); + assert + .dom('[data-test-user-login-via-username-password-username-input]') + .exists(); + + assert + .dom('[data-test-user-login-via-username-password-password-input]') + .exists(); + assert - .dom('[data-test-login-login-username-input]') + .dom('[data-test-user-login-via-username-password-login-button]') + .exists(); + + assert + .dom('[data-test-user-login-via-username-password-username-input]') .hasValue('appknoxusername'); + const passwordLoginInput = this.element.querySelector( - '[data-test-login-login-password-input]' + '[data-test-user-login-via-username-password-password-input]' ); const loginbtn = this.element.querySelector( - '[data-test-login-login-button]' + '[data-test-user-login-via-username-password-login-button]' ); + await fillIn(passwordLoginInput, 'appknoxpassword'); await click(loginbtn); + const session = this.owner.lookup('service:session'); + assert.equal(session.lastUsername, 'appknoxusername'); assert.equal(session.lastPassword, 'appknoxpassword'); }); @@ -189,6 +246,7 @@ module('Integration | Component | login component', function (hooks) { this.server.post('v2/sso/check', function (schema, request) { const body = JSON.parse(request.requestBody); assert.equal(body.username, 'appknoxusername'); + return new Response( 200, {}, @@ -199,6 +257,7 @@ module('Integration | Component | login component', function (hooks) { } ); }); + const session = this.owner.lookup('service:session'); session.authenticateTestHook = function () { const err = new Error('otp required'); @@ -210,47 +269,59 @@ module('Integration | Component | login component', function (hooks) { throw err; }; - await render(hbs``); + await render(hbs``); + const usernameInput = this.element.querySelector( - '[data-test-login-check-username-input]' + '[data-test-user-login-check-type-username-input]' ); const arrowbtn = this.element.querySelector( - '[data-test-login-check-button]' + '[data-test-user-login-check-type-button]' ); + await fillIn(usernameInput, 'appknoxusername'); await click(arrowbtn); + const passwordLoginInput = this.element.querySelector( - '[data-test-login-login-password-input]' + '[data-test-user-login-via-username-password-password-input]' ); const loginbtn = this.element.querySelector( - '[data-test-login-login-button]' + '[data-test-user-login-via-username-password-login-button]' ); + await fillIn(passwordLoginInput, 'appknoxpassword'); await click(loginbtn); + assert.equal(session.lastUsername, 'appknoxusername'); assert.equal(session.lastPassword, 'appknoxpassword'); - assert.dom('[data-test-login-mfa]').exists(); - assert.dom('[data-test-login-mfa-email-otp]').exists(); - assert.dom('[data-test-login-mfa-otp-input]').exists(); - assert.dom('[data-test-login-mfa-button]').exists(); - assert.dom('[data-test-login-mfa-email-otp]').hasText('t:emailOTP:()'); + assert.dom('[data-test-user-login-perform-mfa]').exists(); + assert.dom('[data-test-user-login-perform-mfa-email-otp]').exists(); + assert.dom('[data-test-user-login-perform-mfa-otp-input]').exists(); + assert.dom('[data-test-user-login-perform-mfa-button]').exists(); + assert + .dom('[data-test-user-login-perform-mfa-email-otp]') + .hasText('t:emailOTP:()'); const otpInput = this.element.querySelector( - '[data-test-login-mfa-otp-input]' + '[data-test-user-login-perform-mfa-otp-input]' + ); + const otpbtn = this.element.querySelector( + '[data-test-user-login-perform-mfa-button]' ); - const otpbtn = this.element.querySelector('[data-test-login-mfa-button]'); + await fillIn(otpInput, '462613'); await click(otpbtn); + assert.equal(session.lastUsername, 'appknoxusername'); assert.equal(session.lastPassword, 'appknoxpassword'); assert.equal(session.lastOtp, '462613'); }); - test('it should show otp screen for totp otp flow', async function (assert) { + test('it should show otp screen for authenticator otp flow', async function (assert) { this.server.post('v2/sso/check', function (schema, request) { const body = JSON.parse(request.requestBody); assert.equal(body.username, 'appknoxusername'); + return new Response( 200, {}, @@ -261,6 +332,7 @@ module('Integration | Component | login component', function (hooks) { } ); }); + const session = this.owner.lookup('service:session'); session.authenticateTestHook = function () { const err = new Error('otp required'); @@ -272,46 +344,62 @@ module('Integration | Component | login component', function (hooks) { throw err; }; - await render(hbs``); + await render(hbs``); + const usernameInput = this.element.querySelector( - '[data-test-login-check-username-input]' + '[data-test-user-login-check-type-username-input]' ); const arrowbtn = this.element.querySelector( - '[data-test-login-check-button]' + '[data-test-user-login-check-type-button]' ); + await fillIn(usernameInput, 'appknoxusername'); await click(arrowbtn); + const passwordLoginInput = this.element.querySelector( - '[data-test-login-login-password-input]' + '[data-test-user-login-via-username-password-password-input]' ); const loginbtn = this.element.querySelector( - '[data-test-login-login-button]' + '[data-test-user-login-via-username-password-login-button]' ); + await fillIn(passwordLoginInput, 'appknoxpassword'); await click(loginbtn); + assert.equal(session.lastUsername, 'appknoxusername'); assert.equal(session.lastPassword, 'appknoxpassword'); - assert.dom('[data-test-login-mfa]').exists(); - assert.dom('[data-test-login-mfa-email-otp]').doesNotExist(); - assert.dom('[data-test-login-mfa-otp-input]').exists(); - assert.dom('[data-test-login-mfa-button]').exists(); + assert.dom('[data-test-user-login-perform-mfa]').exists(); + assert + .dom('[data-test-user-login-perform-mfa-authenticator-code]') + .exists(); + + assert.dom('[data-test-user-login-perform-mfa-otp-input]').exists(); + assert.dom('[data-test-user-login-perform-mfa-button]').exists(); + assert + .dom('[data-test-user-login-perform-mfa-authenticator-code]') + .hasText('t:authenticatorCode:()'); const otpInput = this.element.querySelector( - '[data-test-login-mfa-otp-input]' + '[data-test-user-login-perform-mfa-otp-input]' ); - const otpbtn = this.element.querySelector('[data-test-login-mfa-button]'); - await fillIn(otpInput, '462617'); + const otpbtn = this.element.querySelector( + '[data-test-user-login-perform-mfa-button]' + ); + + await fillIn(otpInput, '462613'); await click(otpbtn); + assert.equal(session.lastUsername, 'appknoxusername'); assert.equal(session.lastPassword, 'appknoxpassword'); - assert.equal(session.lastOtp, '462617'); + assert.equal(session.lastOtp, '462613'); }); test('it should show sso button with login form for not forced', async function (assert) { this.server.post('v2/sso/check', function (schema, request) { const body = JSON.parse(request.requestBody); assert.equal(body.username, 'appknoxusername'); + return new Response( 200, {}, @@ -322,26 +410,31 @@ module('Integration | Component | login component', function (hooks) { } ); }); - await render(hbs``); + + await render(hbs``); + const usernameInput = this.element.querySelector( - '[data-test-login-check-username-input]' + '[data-test-user-login-check-type-username-input]' ); const arrowbtn = this.element.querySelector( - '[data-test-login-check-button]' + '[data-test-user-login-check-type-button]' ); + await fillIn(usernameInput, 'appknoxusername'); await click(arrowbtn); - assert.dom('[data-test-login-sso]').doesNotExist(); - assert.dom('[data-test-login-sso-not-forced-login]').exists(); - assert.dom('[data-test-login-sso-or]').exists(); - assert.dom('[data-test-login-sso-not-forced-button]').exists(); - assert.dom('[data-test-login-sso-or]').hasText('t:or:()'); + + assert.dom('[data-test-user-login-via-sso]').doesNotExist(); + assert.dom('[data-test-user-login-via-username-password]').exists(); + assert.dom('[data-test-user-login-via-sso-or]').exists(); + assert.dom('[data-test-user-login-via-sso-not-forced-button]').exists(); + assert.dom('[data-test-user-login-via-sso-or]').hasText('t:or:()'); }); test('it should show sso button without login form for forced', async function (assert) { this.server.post('v2/sso/check', function (schema, request) { const body = JSON.parse(request.requestBody); assert.equal(body.username, 'appknoxusername'); + return new Response( 200, {}, @@ -352,21 +445,28 @@ module('Integration | Component | login component', function (hooks) { } ); }); - await render(hbs``); + + await render(hbs``); + const usernameInput = this.element.querySelector( - '[data-test-login-check-username-input]' + '[data-test-user-login-check-type-username-input]' ); const arrowbtn = this.element.querySelector( - '[data-test-login-check-button]' + '[data-test-user-login-check-type-button]' ); + await fillIn(usernameInput, 'appknoxusername'); await click(arrowbtn); - assert.dom('[data-test-login-sso]').exists(); - assert.dom('[data-test-login-sso-not-forced-login]').doesNotExist(); - assert.dom('[data-test-login-sso-or]').doesNotExist(); - assert.dom('[data-test-login-sso-not-forced-button]').doesNotExist(); - assert.dom('[data-test-login-sso-forced-username-input]').exists(); - assert.dom('[data-test-login-sso-forced-button]').exists(); + + assert.dom('[data-test-user-login-via-sso]').exists(); + assert.dom('[data-test-user-login-via-username-password]').doesNotExist(); + assert.dom('[data-test-user-login-via-sso-or]').doesNotExist(); + assert + .dom('[data-test-user-login-via-sso-not-forced-button]') + .doesNotExist(); + + assert.dom('[data-test-user-login-via-sso-forced-username-input]').exists(); + assert.dom('[data-test-user-login-via-sso-forced-button]').exists(); }); test('it should hide registration link', async function (assert) { @@ -402,10 +502,13 @@ module('Integration | Component | login component', function (hooks) { } ); }); + const configuration = this.owner.lookup('service:configuration'); + await configuration.getFrontendConfig(); - await render(hbs``); - assert.dom('[data-test-login-registration-link]').doesNotExist(); + await render(hbs``); + + assert.dom('[data-test-user-login-registration-link]').doesNotExist(); }); test('it should show native registration link', async function (assert) { @@ -447,12 +550,15 @@ module('Integration | Component | login component', function (hooks) { } ); }); + const configuration = this.owner.lookup('service:configuration'); + await configuration.getFrontendConfig(); - await render(hbs``); - assert.dom('[data-test-login-registration-link]').exists(); + await render(hbs``); + + assert.dom('[data-test-user-login-registration-link]').exists(); assert - .dom('[data-test-login-registration-link]') + .dom('[data-test-user-login-registration-link]') .hasAttribute('href', '/register'); }); @@ -489,12 +595,15 @@ module('Integration | Component | login component', function (hooks) { } ); }); + const configuration = this.owner.lookup('service:configuration'); + await configuration.getFrontendConfig(); - await render(hbs``); - assert.dom('[data-test-login-registration-link]').exists(); + await render(hbs``); + + assert.dom('[data-test-user-login-registration-link]').exists(); assert - .dom('[data-test-login-registration-link]') + .dom('[data-test-user-login-registration-link]') .hasAttribute('href', 'https://example.com/registration'); }); @@ -531,12 +640,15 @@ module('Integration | Component | login component', function (hooks) { } ); }); + const configuration = this.owner.lookup('service:configuration'); + await configuration.getFrontendConfig(); - await render(hbs``); - assert.dom('[data-test-login-registration-link]').exists(); + await render(hbs``); + + assert.dom('[data-test-user-login-registration-link]').exists(); assert - .dom('[data-test-login-registration-link]') + .dom('[data-test-user-login-registration-link]') .hasAttribute('href', 'https://example.com/registration'); }); }); diff --git a/tests/integration/components/user-login/recover-password-test.js b/tests/integration/components/user-login/recover-password-test.js new file mode 100644 index 0000000000..6e8acbf0f5 --- /dev/null +++ b/tests/integration/components/user-login/recover-password-test.js @@ -0,0 +1,136 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, click, fillIn } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupIntl } from 'ember-intl/test-support'; + +import Service from '@ember/service'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { Response } from 'miragejs'; + +class LoggerStub extends Service {} + +class NotificationStub extends Service {} + +module( + 'Integration | Component | user-login/recover-password', + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(async function () { + this.owner.register('service:rollbar', LoggerStub); + this.owner.register('service:notifications', NotificationStub); + }); + + test('it renders password recover page', async function (assert) { + await render(hbs``); + + assert + .dom('[data-test-user-login-recover-password-username-email-input]') + .exists(); + assert.dom('[data-test-user-login-recover-password-reset-btn]').exists(); + + assert + .dom('[data-test-user-login-recover-password-header-text]') + .exists() + .containsText('t:resetPasswordLabel:()'); + + assert.dom('[data-test-user-login-recover-password-footer]').exists(); + }); + + test('should disable button for empty username/email', async function (assert) { + await render(hbs``); + + assert + .dom('[data-test-user-login-recover-password-username-email-input]') + .exists() + .hasNoValue(); + + assert + .dom('[data-test-user-login-recover-password-reset-btn]') + .isDisabled(); + }); + + test('should show message for password recovery mail sent', async function (assert) { + assert.expect(7); + + const username = 'appknox'; + + this.server.post('/v2/forgot_password', (schema, req) => { + const reqUsername = req.requestBody.split('=')[1]; + + assert.strictEqual(username, reqUsername); + + return new Response(200); + }); + + await render(hbs``); + + assert + .dom('[data-test-user-login-recover-password-username-email-input]') + .exists(); + + const usernameEmailInput = this.element.querySelector( + '[data-test-user-login-recover-password-username-email-input]' + ); + + await fillIn(usernameEmailInput, username); + + assert + .dom('[data-test-user-login-recover-password-username-email-input]') + .hasValue(username); + + await click('[data-test-user-login-recover-password-reset-btn]'); + + assert + .dom('[data-test-user-login-recover-password-text-to-check]') + .exists() + .containsText('t:resetPasswordMessageToCheck:()'); + + assert + .dom('[data-test-user-login-recover-password-text-to-retry]') + .exists() + .containsText('t:resetPasswordMessageToRetry:()'); + }); + + test('should hide footer after mail has been sent', async function (assert) { + assert.expect(5); + + const username = 'appknox'; + + this.server.post('/v2/forgot_password', (schema, req) => { + const reqUsername = req.requestBody.split('=')[1]; + + assert.strictEqual(username, reqUsername); + + return new Response(200); + }); + + await render(hbs``); + + assert.dom('[data-test-user-login-recover-password-footer]').exists(); + + assert + .dom('[data-test-user-login-recover-password-username-email-input]') + .exists(); + + const usernameEmailInput = this.element.querySelector( + '[data-test-user-login-recover-password-username-email-input]' + ); + + await fillIn(usernameEmailInput, username); + + assert + .dom('[data-test-user-login-recover-password-username-email-input]') + .hasValue(username); + + await click('[data-test-user-login-recover-password-reset-btn]'); + + assert + .dom('[data-test-user-login-recover-password-footer]') + .doesNotExist(); + }); + } +); diff --git a/translations/en.json b/translations/en.json index d833d210e2..38919977a3 100644 --- a/translations/en.json +++ b/translations/en.json @@ -86,6 +86,8 @@ "asvsExpansion": "Application Security Verification Standard", "attachments": "Attachments", "author": "Author", + "authenticatorCode": "Enter the code from the authenticator in the field below.", + "authenticatorCodeLabel": "Enter the code from the authenticator app", "authorize": "Authorize", "automated": "Automated", "available": "available", @@ -172,6 +174,7 @@ }, "confirmPassword": "Confirm Password", "confirmTransfer": "Confirm Transfer", + "contactSupport": "contact support", "contactUs": "Contact Us", "container": "Container", "continue": "Continue", @@ -182,6 +185,7 @@ "createTeamInputLabel": "Here you can enter the team name", "created": "Created", "createdOn": "Created on", + "credentialsIncorrect": "The credentials you entered is incorrect", "credentialsNotUpdated": "Something went wrong when trying to update credentials", "credentialsUpdated": "Credentials Successfully Updated", "creditAddPurchase": "Please contact support for purchasing scan credits", @@ -293,6 +297,7 @@ "email": "Email", "emailId": "Email ID", "emailCode": "Enter the code from the email in the field below.", + "emailCodeLabel": "Enter the code from the email", "emailDomainRestriction": "Email Domain Restriction for Invitations", "emailOTP": "We've sent a OTP code to your email.", "emailOrUsername": "Email / Username", @@ -309,6 +314,9 @@ "enableMandatoryMFAWarning": "This setting will affect login process of every user of this organization.", "enabled": "Enabled", "enfoceSSO": "Enforce SSO", + "enterCode": "Enter the code", + "enterConfirmPassword": "Enter Confirm Password", + "enterNewPassword": "Enter New Password", "enterNamespace": "Enter the namespace", "enterOTP": "Enter the six digit OTP", "enterRightTeamName": "Enter the right team name to delete it", @@ -490,6 +498,7 @@ "invalidCredits": "Invalid Credits", "invalidOTP": "Please enter a valid OTP", "invalidPassword": "Passwords did not match", + "invalidPasswordResetLink": "It looks like you clicked on an invalid password reset link.", "invalidPayment": "Invalid payment", "invalidProject": "Please select valid project", "invalidRisk": "Please select valid risk value", @@ -543,7 +552,9 @@ "license": "License", "loading": "Loading", "loadingTeam": "Loading Team", + "lockedAccount": "Your account has been locked out. Please reset the password to regain access. If you face any issues, kindly ", "login": "Login", + "loginTitle": "Login to your account", "logout": "Logout", "low": "Low", "manager": "Manager", @@ -872,6 +883,7 @@ "owner": "Owner", "package": "Package", "packageName": "Package Name", + "page": "Page", "pageNotFound": "The page cannot be found", "paid": "Paid", "partnerPrivilege": { @@ -885,6 +897,7 @@ "passwordIsReset": "Password is successfully reset!", "passwordLengthError": "Passwords must be greater than or equal to 6", "passwordMatchError": "Passwords doesnt match", + "passwordPlaceholder": "Enter the Password", "pasteYourUrl": "Paste Android / Windows store URL", "pay": "Pay", "paymentPlan": "Payment plan", @@ -1020,6 +1033,7 @@ "recurring": "recurring", "register": "Register", "registerConfirmation": "Your registration process has been initiated. Please check your email for further instructions", + "registerToday": "Register Today", "registrationRequests": "Registration requests", "regulatory": "Regulatory", "regulatoryPreferenceReset": "Reset preference to default", @@ -1058,7 +1072,10 @@ "resendInvitation": "Resend invitation", "resentInvitationTo": "Resent invitation to", "reset": "Reset", - "resetPassword": "Reset your password", + "resetPassword": "Reset Password", + "resetPasswordLabel": "Reset your password", + "resetPasswordMessageToCheck": "Check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.", + "resetPasswordMessageToRetry": "If you are not receiving new emails after a few attempts, please retry after 24 hours or contact support.", "resetToDefault": "Reset to default", "resetOverriddenAnalysis": "Reset Overridden Analysis", "resolved": "resolved", @@ -1246,6 +1263,7 @@ "tInValidCredentials": "Invalid credentials", "tags": "Tags", "tablet": "Tablet", + "takeMeTo": "Take me to", "team": "Team", "teamCreated": "Team Created Successfully", "teamDetails": "Team Details", @@ -1328,8 +1346,11 @@ "userSetting": " user's settings ", "usersTeamDesc": "List of Teams the user is associated with", "username": "Username", + "usernameEmailIdTextLabel": "Username/Email ID", + "usernameEmailIdTextPlaceholder": "Enter your Username/Email ID", "validUntil": "Valid until", "validity": "Validity", + "verify": "Verify", "version": "Version", "versions": "Versions", "versionLowercase": "version", diff --git a/translations/ja.json b/translations/ja.json index df91f55f71..f3a5adc75f 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -86,6 +86,8 @@ "asvsExpansion": "Application Security Verification Standard", "attachments": "添付ファイル", "author": "Author", + "authenticatorCode": "Enter the code from the authenticator in the field below.", + "authenticatorCodeLabel": "Enter the code from the authenticator app", "authorize": "Authorize", "automated": "Automated", "available": "available", @@ -172,6 +174,7 @@ }, "confirmPassword": "新しいパスワード(確認)", "confirmTransfer": "Confirm Transfer", + "contactSupport": "contact support", "contactUs": "Contact Us", "container": "Container", "continue": "続ける", @@ -182,6 +185,7 @@ "createTeamInputLabel": "Here you can enter the team name", "created": "作成されました", "createdOn": "作成日", + "credentialsIncorrect": "The credentials you entered is incorrect", "credentialsNotUpdated": "資格情報の更新中に問題が発生しました", "credentialsUpdated": "資格情報が更新されました", "creditAddPurchase": "Please contact support for purchasing scan credits", @@ -293,6 +297,7 @@ "email": "Eメール", "emailId": "Email ID", "emailCode": "Enter the code from the email in the field below.", + "emailCodeLabel": "Enter the code from the email", "emailDomainRestriction": "Email Domain Restriction for Invitations", "emailOTP": "We've sent a OTP code to your email.", "emailOrUsername": "Eメール/ユーザ名", @@ -309,6 +314,9 @@ "enableMandatoryMFAWarning": "This setting will affect login process of every user of this organization.", "enabled": "有効", "enfoceSSO": "Enforce SSO", + "enterCode": "Enter the code", + "enterConfirmPassword": "Enter Confirm Password", + "enterNewPassword": "Enter New Password", "enterNamespace": "Enter the namespace", "enterOTP": "6桁のOTPを入力してください", "enterRightTeamName": "削除するチーム名を正確に入力してください", @@ -490,6 +498,7 @@ "invalidCredits": "Invalid Credits", "invalidOTP": "Please enter a valid OTP", "invalidPassword": "パスワードが正しくありません", + "invalidPasswordResetLink": "It looks like you clicked on an invalid password reset link.", "invalidPayment": "Invalid payment", "invalidProject": "Please select valid project", "invalidRisk": "Please select valid risk value", @@ -543,7 +552,9 @@ "license": "License", "loading": "読み込み中", "loadingTeam": "Loading Team", + "lockedAccount": "Your account has been locked out. Please reset the password to regain access. If you face any issues, kindly ", "login": "ログイン", + "loginTitle": "Login to your account", "logout": "ログアウト", "low": "低", "manager": "マネージャー", @@ -872,6 +883,7 @@ "owner": "オーナー", "package": "パッケージ", "packageName": "パッケージ名", + "page": "Page", "pageNotFound": "ページが見つかりません", "paid": "有料", "partnerPrivilege": { @@ -885,6 +897,7 @@ "passwordIsReset": "パスワードは正常にリセットされました", "passwordLengthError": "パスワードは6桁以上でなければなりません", "passwordMatchError": "パスワードが一致しません", + "passwordPlaceholder": "Enter the Password", "pasteYourUrl": "Android / WindowsのストアURLを貼り付けてください", "pay": "支払い請求周期", "paymentPlan": "Payment plan", @@ -1020,6 +1033,7 @@ "recurring": "recurring", "register": "登録", "registerConfirmation": "Your registration process has been initiated. Please check your mail for further instructions", + "registerToday": "Register Today", "registrationRequests": "Registration requests", "regulatory": "Regulatory", "regulatoryPreferenceReset": "Reset preference to default", @@ -1058,7 +1072,10 @@ "resendInvitation": "Resend invitation", "resentInvitationTo": "Resent invitation to", "reset": "リセット", - "resetPassword": "パスワードのリセット", + "resetPassword": "Reset Password", + "resetPasswordLabel": "パスワードのリセット", + "resetPasswordMessageToCheck": "Check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.", + "resetPasswordMessageToRetry": "If you are not receiving new emails after a few attempts, please retry after 24 hours or contact support.", "resetToDefault": "Reset to default", "resetOverriddenAnalysis": "Reset Overridden Analysis", "resolved": "resolved", @@ -1246,6 +1263,7 @@ "tInValidCredentials": "Invalid credentials", "tags": "Tags", "tablet": "タブレット", + "takeMeTo": "Take me to", "team": "チーム", "teamCreated": "チームが作成されました", "teamDeleted": "削除されました", @@ -1328,8 +1346,11 @@ "userSetting": "ユーザ設定", "usersTeamDesc": "List of Teams the user is associated with", "username": "ユーザ名", + "usernameEmailIdTextLabel": "Username/Email ID", + "usernameEmailIdTextPlaceholder": "Enter your Username/Email ID", "validUntil": "Valid until", "validity": "Validity", + "verify": "Verify", "version": "バージョン", "versions": "Versions", "versionLowercase": "バージョン",