diff --git a/core/app/core/src/lib/core.ts b/core/app/core/src/lib/core.ts
index 572b37255..8e4c7c9dd 100644
--- a/core/app/core/src/lib/core.ts
+++ b/core/app/core/src/lib/core.ts
@@ -725,3 +725,6 @@ export * from './views/2fa/components/2fa/2fa.component';
export * from './views/2fa/components/2fa/2fa.module';
export * from './views/2fa/components/2fa-check/2fa-check.component';
export * from './views/2fa/components/2fa-check/2fa-check.module';
+export * from './views/2fa/components/2fa-check-modal/2fa-check-modal.component';
+export * from './views/2fa/components/2fa-check-modal/2fa-check-modal.module';
+export * from './views/2fa/components/2fa-check-modal/2fa-check-modal.model';
diff --git a/core/app/core/src/lib/services/process/processes/check-two-factor-code/check-two-factor-code.ts b/core/app/core/src/lib/services/process/processes/check-two-factor-code/check-two-factor-code.ts
new file mode 100644
index 000000000..a3bc434ce
--- /dev/null
+++ b/core/app/core/src/lib/services/process/processes/check-two-factor-code/check-two-factor-code.ts
@@ -0,0 +1,54 @@
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2024 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+import {Injectable} from '@angular/core';
+import {take} from 'rxjs/operators';
+import {Process, ProcessService} from "../../process.service";
+import {Observable} from "rxjs";
+
+@Injectable({providedIn: 'root'})
+export class CheckTwoFactorCode {
+
+
+ constructor(
+ protected processService: ProcessService,
+ ) {
+ }
+
+ /**
+ * Check Auth Code
+ */
+ public checkCode(auth_code): Observable {
+
+ const processType = 'check-two-factor-code';
+
+ const options = {
+ auth_code
+ };
+
+ return this.processService.submit(processType, options).pipe(take(1));
+ }
+}
diff --git a/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.html b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.html
new file mode 100644
index 000000000..a0b96e013
--- /dev/null
+++ b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.ts b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.ts
new file mode 100644
index 000000000..af88f3fc1
--- /dev/null
+++ b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.ts
@@ -0,0 +1,76 @@
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2024 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+import {Component, HostListener} from "@angular/core";
+import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
+import {LanguageStore} from "../../../../store/language/language.store";
+import {CheckTwoFactorCode} from "../../../../services/process/processes/check-two-factor-code/check-two-factor-code";
+import {TwoFactorCheckModalResult} from "./2fa-check-modal.model";
+import {MessageService} from "../../../../services/message/message.service";
+
+@Component({
+ selector: 'scrm-2fa-modal',
+ templateUrl: './2fa-check-modal.component.html',
+ styleUrls: [],
+})
+export class TwoFactorCheckModalComponent {
+
+ authCode: string;
+
+
+ @HostListener('keyup.control.enter')
+ onEnterKey() {
+ this.checkCode();
+ }
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ protected language: LanguageStore,
+ protected message: MessageService,
+ protected checkTwoFactorCode: CheckTwoFactorCode
+ ) {
+ }
+
+
+ public checkCode() {
+ const authCode = this.authCode;
+
+ this.checkTwoFactorCode.checkCode(authCode).subscribe({
+ next: (response) => {
+ this.closeModal(response.data.two_factor_complete)
+ },
+ error: () => {
+ this.message.addDangerMessageByKey('LBL_FACTOR_AUTH_FAIL')
+ }
+ });
+ }
+
+ public closeModal(authComplete: boolean) {
+ this.activeModal.close({
+ two_factor_complete: authComplete
+ } as TwoFactorCheckModalResult);
+ }
+
+}
diff --git a/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.model.ts b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.model.ts
new file mode 100644
index 000000000..ed614a3c4
--- /dev/null
+++ b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.model.ts
@@ -0,0 +1,31 @@
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2024 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+export interface TwoFactorCheckModalResult {
+ [key: string]: any;
+
+ two_factor_complete: boolean
+}
diff --git a/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.module.ts b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.module.ts
new file mode 100644
index 000000000..b8f780a6d
--- /dev/null
+++ b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.module.ts
@@ -0,0 +1,48 @@
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2024 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+import {NgModule} from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {ModalModule} from "../../../../components/modal/components/modal/modal.module";
+import {TwoFactorCheckModalComponent} from "./2fa-check-modal.component";
+import {TwoFactorCheckModule} from "../2fa-check/2fa-check.module";
+import {FormsModule} from "@angular/forms";
+import {LabelModule} from "../../../../components/label/label.module";
+import {TrustHtmlModule} from "../../../../pipes/trust-html/trust-html.module";
+
+@NgModule({
+ declarations: [TwoFactorCheckModalComponent],
+ imports: [
+ CommonModule,
+ ModalModule,
+ TwoFactorCheckModule,
+ FormsModule,
+ LabelModule,
+ TrustHtmlModule,
+ ]
+})
+export class TwoFactorCheckModalModule {
+}
diff --git a/core/app/core/src/lib/views/2fa/components/2fa/2fa.component.ts b/core/app/core/src/lib/views/2fa/components/2fa/2fa.component.ts
index 741b77543..6b7e50d4a 100644
--- a/core/app/core/src/lib/views/2fa/components/2fa/2fa.component.ts
+++ b/core/app/core/src/lib/views/2fa/components/2fa/2fa.component.ts
@@ -34,6 +34,8 @@ import {ButtonCallback, ButtonInterface} from "../../../../common/components/but
import {UserPreferenceStore} from "../../../../store/user-preference/user-preference.store";
import {Clipboard} from '@angular/cdk/clipboard';
import {GenerateBackupCodes} from "../../../../services/process/processes/generate-backup-codes/generate-backup-codes";
+import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
+import {TwoFactorCheckModalComponent} from "../2fa-check-modal/2fa-check-modal.component";
@Component({
@@ -70,6 +72,7 @@ export class TwoFactorComponent implements OnInit {
protected message: MessageService,
protected language: LanguageStore,
protected userPreference: UserPreferenceStore,
+ protected modalService: NgbModal,
protected clipboard: Clipboard,
protected generateBackupCodesService: GenerateBackupCodes,
) {
@@ -117,7 +120,7 @@ export class TwoFactorComponent implements OnInit {
onClick: ((): void => {
this.generateBackupCodes();
}) as ButtonCallback,
- labelKey: 'LBL_REGENERATE_BACKUP_CODES',
+ labelKey: 'LBL_REGENERATE_CODES',
titleKey: ''
} as ButtonInterface;
}
@@ -140,35 +143,46 @@ export class TwoFactorComponent implements OnInit {
}
public disable2FactorAuth(): void {
- this.authService.disable2fa().subscribe({
- next: (response) => {
- if (isTrue(response?.two_factor_disabled)) {
+ const modal = this.modalService.open(TwoFactorCheckModalComponent, {size: 'lg'});
- this.isAppMethodEnabled.set(false);
- this.areRecoveryCodesGenerated.set(false);
- this.isQrCodeGenerated.set(false);
- this.message.addSuccessMessageByKey('LBL_FACTOR_AUTH_DISABLE');
- }
- },
- error: () => {
- this.isAppMethodEnabled.set(true);
- this.areRecoveryCodesGenerated.set(true);
+ modal.result.then((result) => {
+ if (!result.two_factor_complete){
+ this.message.addDangerMessageByKey('LBL_FACTOR_AUTH_FAIL');
+ return;
}
- });
- return;
+
+ this.authService.disable2fa().subscribe({
+ next: (response) => {
+ if (isTrue(response?.two_factor_disabled)) {
+
+ this.isAppMethodEnabled.set(false);
+ this.areRecoveryCodesGenerated.set(false);
+ this.isQrCodeGenerated.set(false);
+
+ this.message.addSuccessMessageByKey('LBL_FACTOR_AUTH_DISABLE');
+ }
+ },
+ error: () => {
+ this.isAppMethodEnabled.set(true);
+ this.areRecoveryCodesGenerated.set(true);
+ }
+ });
+ return;
+ }).catch();
+
}
getTitle(): string {
return this.title;
}
- public finalize2fa() {
+ public finalize2fa(): void {
this.authService.finalize2fa(this.authCode).subscribe(response => {
const verified = response?.two_factor_setup_complete ?? false;
if (isTrue(verified)) {
- this.generateBackupCodes();
+ this.generateCodes();
this.message.addSuccessMessageByKey('LBL_FACTOR_AUTH_SUCCESS');
this.isAppMethodEnabled.set(true);
@@ -182,24 +196,36 @@ export class TwoFactorComponent implements OnInit {
})
}
- public copyBackupCodes() {
+ public copyBackupCodes(): void {
this.clipboard.copy(this.backupCodes);
}
-
- public generateBackupCodes(){
- this.areRecoveryCodesGenerated.set(false)
+ public generateCodes(): void {
this.generateBackupCodesService.generate().subscribe({
next: (response) => {
- console.log(response);
- console.log('inside next');
this.backupCodes = response?.data.backupCodes;
this.areRecoveryCodesGenerated.set(true)
},
error: () => {
- console.log('inside eror');
this.areRecoveryCodesGenerated.set(false)
}
});
+ return;
+ }
+
+
+ public generateBackupCodes(): void {
+
+ const modal = this.modalService.open(TwoFactorCheckModalComponent, {size: 'lg'});
+
+ modal.result.then((result) => {
+ if (!result.two_factor_complete){
+ this.message.addDangerMessageByKey('LBL_FACTOR_AUTH_FAIL');
+ return;
+ }
+
+ this.areRecoveryCodesGenerated.set(false)
+ this.generateCodes()
+ }).catch();
}
}
diff --git a/core/app/shell/src/themes/suite8/css/layout/_two-factor.scss b/core/app/shell/src/themes/suite8/css/layout/_two-factor.scss
index 1d32aaa23..64157b777 100644
--- a/core/app/shell/src/themes/suite8/css/layout/_two-factor.scss
+++ b/core/app/shell/src/themes/suite8/css/layout/_two-factor.scss
@@ -1,3 +1,30 @@
+.two-factor-popup {
+ .auth-input {
+ border: .03em solid $nepal-grey;
+ padding: .45em;
+ margin: 0 1em 0 0;
+ color: #666;
+ font-size: .8em;
+ background-color: #fff;
+ width: 30%;
+ text-align: center;
+ }
+}
+
+@media (min-width: 991px) {
+ .backup-codes-container {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 10px;
+
+ .backup-codes {
+ padding: 10px;
+ border: 1px solid lightgrey;
+ border-radius: .25rem;
+ }
+ }
+}
+
@media (min-width: 768px) {
#two-factor {
@@ -8,6 +35,7 @@
width: fit-content;
margin-left: 1.5rem;
}
+
.qr-code-container {
display: flex;
padding: .25rem 1.5rem .5rem 1.5rem;
@@ -39,15 +67,17 @@
}
}
- .backup-codes-container {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 10px;
+ @media (max-width: 991px) {
+ .backup-codes-container {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 10px;
- .backup-codes {
- padding: 10px;
- border: 1px solid lightgrey;
- border-radius: .25rem;
+ .backup-codes {
+ padding: 10px;
+ border: 1px solid lightgrey;
+ border-radius: .25rem;
+ }
}
}
@@ -68,6 +98,7 @@
margin-left: 1.5rem;
margin-right: 2rem;
}
+
.qr-code-container {
display: flex;
flex-direction: column;
@@ -122,6 +153,7 @@
}
}
}
+
@media(max-width: 374px) {
#two-factor {
@@ -132,6 +164,7 @@
margin-left: 1.5rem;
margin-right: 2rem;
}
+
.qr-code-container {
display: flex;
flex-direction: column;
diff --git a/core/backend/Process/LegacyHandler/CheckTwoFactorCodeHandler.php b/core/backend/Process/LegacyHandler/CheckTwoFactorCodeHandler.php
new file mode 100644
index 000000000..54e7affea
--- /dev/null
+++ b/core/backend/Process/LegacyHandler/CheckTwoFactorCodeHandler.php
@@ -0,0 +1,130 @@
+.
+ *
+ * In accordance with Section 7(b) of the GNU Affero General Public License
+ * version 3, these Appropriate Legal Notices must retain the display of the
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+namespace App\Process\LegacyHandler;
+
+use ApiPlatform\Exception\InvalidArgumentException;
+use App\Engine\LegacyHandler\LegacyHandler;
+use App\Engine\LegacyHandler\LegacyScopeState;
+use App\Process\Entity\Process;
+use App\Process\Service\ProcessHandlerInterface;
+use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+class CheckTwoFactorCodeHandler extends LegacyHandler implements ProcessHandlerInterface
+{
+ protected const MSG_OPTIONS_NOT_FOUND = 'Process options is not defined';
+ protected const PROCESS_TYPE = 'check-two-factor-code';
+
+
+ protected TotpAuthenticatorInterface $totpAuthenticator;
+ protected Security $security;
+
+
+ public function __construct(
+ string $projectDir,
+ string $legacyDir,
+ string $legacySessionName,
+ string $defaultSessionName,
+ LegacyScopeState $legacyScopeState,
+ RequestStack $requestStack,
+ Security $security,
+ TotpAuthenticatorInterface $totpAuthenticator,
+ ) {
+ parent::__construct(
+ $projectDir,
+ $legacyDir,
+ $legacySessionName,
+ $defaultSessionName,
+ $legacyScopeState,
+ $requestStack
+ );
+ $this->security = $security;
+ $this->totpAuthenticator = $totpAuthenticator;
+ }
+
+ public function getHandlerKey(): string
+ {
+ return self::PROCESS_TYPE;
+ }
+
+ public function getProcessType(): string
+ {
+ return self::PROCESS_TYPE;
+ }
+
+ public function requiredAuthRole(): string
+ {
+ return 'ROLE_USER';
+ }
+
+ public function getRequiredACLs(Process $process): array
+ {
+ return [];
+ }
+
+ public function configure(Process $process): void
+ {
+ $process->setId(self::PROCESS_TYPE);
+ $process->setAsync(false);
+ }
+
+ public function validate(Process $process): void
+ {
+ if (empty($process->getOptions())) {
+ throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
+ }
+ }
+
+ public function run(Process $process): void
+ {
+ $options = $process->getOptions();
+
+ $authCode = $options['auth_code'] ?? false;
+
+ $user = $this->security->getToken()->getUser();
+
+ if (!$options['auth_code']) {
+ $process->setStatus('error');
+ $process->setMessages(['LBL_ACTION_ERROR']);
+
+ return;
+ }
+
+ $correctCode = $this->totpAuthenticator->checkCode($user, $authCode);
+
+ if (!$correctCode){
+ $correctCode = $user->isBackupCode($authCode);
+ }
+
+ $response = ['two_factor_complete' => $correctCode];
+
+ $process->setData($response);
+
+ }
+}