diff --git a/apps/web/src/app/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/organizations/settings/two-factor-setup.component.ts index 12675fcf52..a8566e12db 100644 --- a/apps/web/src/app/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/organizations/settings/two-factor-setup.component.ts @@ -3,7 +3,10 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "jslib-angular/services/modal.service"; import { ApiService } from "jslib-common/abstractions/api.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { PolicyService } from "jslib-common/abstractions/policy.service"; import { StateService } from "jslib-common/abstractions/state.service"; import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; @@ -22,9 +25,21 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { messagingService: MessagingService, policyService: PolicyService, private route: ActivatedRoute, - stateService: StateService + stateService: StateService, + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + logService: LogService ) { - super(apiService, modalService, messagingService, policyService, stateService); + super( + apiService, + modalService, + messagingService, + policyService, + stateService, + platformUtilsService, + i18nService, + logService + ); } async ngOnInit() { diff --git a/apps/web/src/app/settings/two-factor-setup.component.html b/apps/web/src/app/settings/two-factor-setup.component.html index 27fc2990e6..8a4a1f698c 100644 --- a/apps/web/src/app/settings/two-factor-setup.component.html +++ b/apps/web/src/app/settings/two-factor-setup.component.html @@ -55,6 +55,47 @@ +
+
+
+

+ {{ "deviceVerification" | i18n }} +

+
+
+ + +
+ {{ "deviceVerificationDesc" | i18n }} +
+ +
+
+
diff --git a/apps/web/src/app/settings/two-factor-setup.component.ts b/apps/web/src/app/settings/two-factor-setup.component.ts index bab2594237..d83177e13c 100644 --- a/apps/web/src/app/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/settings/two-factor-setup.component.ts @@ -3,11 +3,15 @@ import { Component, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/c import { ModalRef } from "jslib-angular/components/modal/modal.ref"; import { ModalService } from "jslib-angular/services/modal.service"; import { ApiService } from "jslib-common/abstractions/api.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { PolicyService } from "jslib-common/abstractions/policy.service"; import { StateService } from "jslib-common/abstractions/state.service"; import { PolicyType } from "jslib-common/enums/policyType"; import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; +import { DeviceVerificationRequest } from "jslib-common/models/request/deviceVerificationRequest"; import { TwoFactorProviders } from "jslib-common/services/twoFactor.service"; import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component"; @@ -39,18 +43,32 @@ export class TwoFactorSetupComponent implements OnInit { canAccessPremium: boolean; showPolicyWarning = false; loading = true; + enableDeviceVerification: boolean; + isDeviceVerificationSectionEnabled: boolean; modal: ModalRef; + formPromise: Promise; constructor( protected apiService: ApiService, protected modalService: ModalService, protected messagingService: MessagingService, protected policyService: PolicyService, - private stateService: StateService + private stateService: StateService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private logService: LogService ) {} async ngOnInit() { this.canAccessPremium = await this.stateService.getCanAccessPremium(); + try { + const deviceVerificationSettings = await this.apiService.getDeviceVerificationSettings(); + this.isDeviceVerificationSectionEnabled = + deviceVerificationSettings.isDeviceVerificationSectionEnabled; + this.enableDeviceVerification = deviceVerificationSettings.unknownDeviceVerificationEnabled; + } catch (e) { + this.logService.error(e); + } for (const key in TwoFactorProviders) { // eslint-disable-next-line @@ -186,4 +204,37 @@ export class TwoFactorSetupComponent implements OnInit { this.showPolicyWarning = false; } } + + async submit() { + try { + if (this.enableDeviceVerification) { + const email = await this.stateService.getEmail(); + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t( + "areYouSureYouWantToEnableDeviceVerificationTheVerificationCodeEmailsWillArriveAtX", + email + ), + this.i18nService.t("deviceVerification"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return; + } + } + + this.formPromise = this.apiService.putDeviceVerificationSettings( + new DeviceVerificationRequest(this.enableDeviceVerification) + ); + await this.formPromise; + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("updatedDeviceVerification") + ); + } catch (e) { + this.logService.error(e); + } + } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 53cbcec8d4..a930e76fbf 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5075,5 +5075,26 @@ }, "apiAccessToken": { "message": "API Access Token" + }, + "deviceVerification": { + "message": "Device Verification" + }, + "enableDeviceVerification": { + "message": "Enable Device Verification" + }, + "deviceVerificationDesc": { + "message": "When enabled, verification codes are sent to your email address when logging in from an unrecognized device" + }, + "updatedDeviceVerification": { + "message": "Updated Device Verification" + }, + "areYouSureYouWantToEnableDeviceVerificationTheVerificationCodeEmailsWillArriveAtX": { + "message": "Are you sure you want to enable Device Verification? The verification code emails will arrive at: $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "My Email" + } + } } } \ No newline at end of file diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 21a5373aa7..bda6e2d0a5 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -1,7 +1,9 @@ import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType"; +import { DeviceVerificationRequest } from "jslib-common/models/request/deviceVerificationRequest"; import { OrganizationConnectionRequest } from "jslib-common/models/request/organizationConnectionRequest"; import { BillingHistoryResponse } from "jslib-common/models/response/billingHistoryResponse"; import { BillingPaymentResponse } from "jslib-common/models/response/billingPaymentResponse"; +import { DeviceVerificationResponse } from "jslib-common/models/response/deviceVerificationResponse"; import { OrganizationConnectionConfigApis, OrganizationConnectionResponse, @@ -494,6 +496,10 @@ export abstract class ApiService { postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise; postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; + getDeviceVerificationSettings: () => Promise; + putDeviceVerificationSettings: ( + request: DeviceVerificationRequest + ) => Promise; getEmergencyAccessTrusted: () => Promise>; getEmergencyAccessGranted: () => Promise>; diff --git a/libs/common/src/models/request/deviceVerificationRequest.ts b/libs/common/src/models/request/deviceVerificationRequest.ts new file mode 100644 index 0000000000..5e119efa19 --- /dev/null +++ b/libs/common/src/models/request/deviceVerificationRequest.ts @@ -0,0 +1,7 @@ +export class DeviceVerificationRequest { + unknownDeviceVerificationEnabled: boolean; + + constructor(unknownDeviceVerificationEnabled: boolean) { + this.unknownDeviceVerificationEnabled = unknownDeviceVerificationEnabled; + } +} diff --git a/libs/common/src/models/response/deviceVerificationResponse.ts b/libs/common/src/models/response/deviceVerificationResponse.ts new file mode 100644 index 0000000000..65762bb9b6 --- /dev/null +++ b/libs/common/src/models/response/deviceVerificationResponse.ts @@ -0,0 +1,16 @@ +import { BaseResponse } from "./baseResponse"; + +export class DeviceVerificationResponse extends BaseResponse { + isDeviceVerificationSectionEnabled: boolean; + unknownDeviceVerificationEnabled: boolean; + + constructor(response: any) { + super(response); + this.isDeviceVerificationSectionEnabled = this.getResponseProperty( + "IsDeviceVerificationSectionEnabled" + ); + this.unknownDeviceVerificationEnabled = this.getResponseProperty( + "UnknownDeviceVerificationEnabled" + ); + } +} diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 2e0e3b92cf..abb4bf485e 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -31,6 +31,7 @@ import { CipherRequest } from "../models/request/cipherRequest"; import { CipherShareRequest } from "../models/request/cipherShareRequest"; import { CollectionRequest } from "../models/request/collectionRequest"; import { DeleteRecoverRequest } from "../models/request/deleteRecoverRequest"; +import { DeviceVerificationRequest } from "../models/request/deviceVerificationRequest"; import { EmailRequest } from "../models/request/emailRequest"; import { EmailTokenRequest } from "../models/request/emailTokenRequest"; import { EmergencyAccessAcceptRequest } from "../models/request/emergencyAccessAcceptRequest"; @@ -121,6 +122,7 @@ import { CollectionGroupDetailsResponse, CollectionResponse, } from "../models/response/collectionResponse"; +import { DeviceVerificationResponse } from "../models/response/deviceVerificationResponse"; import { DomainsResponse } from "../models/response/domainsResponse"; import { EmergencyAccessGranteeDetailsResponse, @@ -1572,6 +1574,30 @@ export class ApiService implements ApiServiceAbstraction { return this.send("POST", "/two-factor/send-email-login", request, false, false); } + async getDeviceVerificationSettings(): Promise { + const r = await this.send( + "GET", + "/two-factor/get-device-verification-settings", + null, + true, + true + ); + return new DeviceVerificationResponse(r); + } + + async putDeviceVerificationSettings( + request: DeviceVerificationRequest + ): Promise { + const r = await this.send( + "PUT", + "/two-factor/device-verification-settings", + request, + true, + true + ); + return new DeviceVerificationResponse(r); + } + // Emergency Access APIs async getEmergencyAccessTrusted(): Promise> {