diff --git a/bitwarden_license/src/app/app.component.ts b/bitwarden_license/src/app/app.component.ts new file mode 100644 index 00000000..33c6e828 --- /dev/null +++ b/bitwarden_license/src/app/app.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +import { AppComponent as BaseAppComponent } from 'src/app/app.component'; +import { MaximumVaultTimeoutPolicy } from './policies/maximum-vault-timeout.component'; + +@Component({ + selector: 'app-root', + templateUrl: '../../../src/app/app.component.html', +}) +export class AppComponent extends BaseAppComponent { + + ngOnInit() { + super.ngOnInit(); + + this.policyListService.addPolicies([ + new MaximumVaultTimeoutPolicy(), + ]); + } + +} diff --git a/bitwarden_license/src/app/app.module.ts b/bitwarden_license/src/app/app.module.ts index cdf705c1..c69bc1b3 100644 --- a/bitwarden_license/src/app/app.module.ts +++ b/bitwarden_license/src/app/app.module.ts @@ -3,27 +3,36 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterModule } from '@angular/router'; import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { MaximumVaultTimeoutPolicyComponent } from './policies/maximum-vault-timeout.component'; -import { AppComponent } from 'src/app/app.component'; import { OssRoutingModule } from 'src/app/oss-routing.module'; import { OssModule } from 'src/app/oss.module'; import { ServicesModule } from 'src/app/services/services.module'; + @NgModule({ imports: [ OssModule, BrowserAnimationsModule, FormsModule, + ReactiveFormsModule, ServicesModule, ToasterModule.forRoot(), InfiniteScrollModule, DragDropModule, AppRoutingModule, OssRoutingModule, + RouterModule, + ], + declarations: [ + AppComponent, + MaximumVaultTimeoutPolicyComponent, ], bootstrap: [AppComponent], }) diff --git a/bitwarden_license/src/app/policies/maximum-vault-timeout.component.html b/bitwarden_license/src/app/policies/maximum-vault-timeout.component.html new file mode 100644 index 00000000..ac43f2d1 --- /dev/null +++ b/bitwarden_license/src/app/policies/maximum-vault-timeout.component.html @@ -0,0 +1,27 @@ + + {{'requireSsoPolicyReq' | i18n}} + + +
+
+ + +
+
+ +
+
+ +
+
+ + {{'hours' | i18n }} +
+
+ + {{'minutes' | i18n }} +
+
+
+
diff --git a/bitwarden_license/src/app/policies/maximum-vault-timeout.component.ts b/bitwarden_license/src/app/policies/maximum-vault-timeout.component.ts new file mode 100644 index 00000000..496cf5a3 --- /dev/null +++ b/bitwarden_license/src/app/policies/maximum-vault-timeout.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; + +import { PolicyType } from 'jslib-common/enums/policyType'; + +import { PolicyRequest } from 'jslib-common/models/request/policyRequest'; + +import { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component'; + +export class MaximumVaultTimeoutPolicy extends BasePolicy { + name = 'maximumVaultTimeout'; + description = 'maximumVaultTimeoutDesc'; + type = PolicyType.MaximumVaultTimeout; + component = MaximumVaultTimeoutPolicyComponent; +} + +@Component({ + selector: 'policy-maximum-timeout', + templateUrl: 'maximum-vault-timeout.component.html', +}) +export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent { + + data = this.fb.group({ + hours: [null], + minutes: [null], + }); + + constructor(private fb: FormBuilder, private i18nService: I18nService) { + super(); + } + + loadData() { + const minutes = this.policyResponse.data?.minutes; + + if (minutes == null) { + return; + } + + this.data.patchValue({ + hours: Math.floor(minutes / 60), + minutes: minutes % 60, + }); + } + + buildRequestData() { + if (this.data.value.hours == null && this.data.value.minutes == null) { + return null; + } + + return { + minutes: this.data.value.hours * 60 + this.data.value.minutes, + }; + } + + buildRequest(policiesEnabledMap: Map): Promise { + const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false; + if (this.enabled.value && !singleOrgEnabled) { + throw new Error(this.i18nService.t('requireSsoPolicyReqError')); + } + + return super.buildRequest(policiesEnabledMap); + } +} diff --git a/jslib b/jslib index 5f64d956..32774561 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 5f64d956520612a681611a27c5f4f2e5f27b640e +Subproject commit 32774561f37bdcf9abb80276c5d1958b7ec192de diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1066149b..a8e2f88a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -22,6 +22,9 @@ import { ServicesModule } from './services/services.module'; DragDropModule, OssRoutingModule, ], + declarations: [ + AppComponent, + ], bootstrap: [AppComponent], }) export class AppModule { } diff --git a/src/app/organizations/policies/base-policy.component.ts b/src/app/organizations/policies/base-policy.component.ts index bc4b3e3d..c42647a7 100644 --- a/src/app/organizations/policies/base-policy.component.ts +++ b/src/app/organizations/policies/base-policy.component.ts @@ -35,19 +35,28 @@ export abstract class BasePolicyComponent implements OnInit { ngOnInit(): void { this.enabled.setValue(this.policyResponse.enabled); - if (this.data != null) { - this.data.patchValue(this.policyResponse.data ?? {}); + if (this.policyResponse.data != null) { + this.loadData(); } } + loadData() { + this.data.patchValue(this.policyResponse.data ?? {}); + } + + buildRequestData() { + if (this.data != null) { + return this.data.value; + } + + return null; + } + buildRequest(policiesEnabledMap: Map) { const request = new PolicyRequest(); request.enabled = this.enabled.value; request.type = this.policy.type; - - if (this.data != null) { - request.data = this.data.value; - } + request.data = this.buildRequestData(); return Promise.resolve(request); } diff --git a/src/app/oss.module.ts b/src/app/oss.module.ts index 06b73019..3ea40c4e 100644 --- a/src/app/oss.module.ts +++ b/src/app/oss.module.ts @@ -11,8 +11,6 @@ import { RouterModule } from '@angular/router'; import { ToasterModule } from 'angular2-toaster'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { AppComponent } from './app.component'; - import { AvatarComponent } from './components/avatar.component'; import { PasswordRepromptComponent } from './components/password-reprompt.component'; import { PasswordStrengthComponent } from './components/password-strength.component'; @@ -144,6 +142,7 @@ import { UpdateKeyComponent } from './settings/update-key.component'; import { UpdateLicenseComponent } from './settings/update-license.component'; import { UserBillingComponent } from './settings/user-billing.component'; import { UserSubscriptionComponent } from './settings/user-subscription.component'; +import { VaultTimeoutInputComponent } from './settings/vault-timeout-input.component'; import { VerifyEmailComponent } from './settings/verify-email.component'; import { BreachReportComponent } from './tools/breach-report.component'; @@ -307,7 +306,6 @@ registerLocaleData(localeZhTw, 'zh-TW'); AdjustStorageComponent, ApiActionDirective, ApiKeyComponent, - AppComponent, AttachmentsComponent, AutofocusDirective, AvatarComponent, @@ -457,6 +455,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); DisableSendPolicyComponent, SendOptionsPolicyComponent, ResetPasswordPolicyComponent, + VaultTimeoutInputComponent, ], exports: [ A11yTitleDirective, diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index 6639268f..144fe7db 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -124,7 +124,7 @@ const sendService = new SendService(cryptoService, userService, apiService, file i18nService, cryptoFunctionService); const vaultTimeoutService = new VaultTimeoutService(cipherService, folderService, collectionService, cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, tokenService, - null, async () => messagingService.send('logout', { expired: false })); + policyService, null, async () => messagingService.send('logout', { expired: false })); const syncService = new SyncService(userService, apiService, settingsService, folderService, cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService, async (expired: boolean) => messagingService.send('logout', { expired: expired })); diff --git a/src/app/settings/options.component.html b/src/app/settings/options.component.html index b369b90f..8619be27 100644 --- a/src/app/settings/options.component.html +++ b/src/app/settings/options.component.html @@ -5,13 +5,8 @@
-
- - - {{'vaultTimeoutDesc' | i18n}} -
+ +
diff --git a/src/app/settings/options.component.ts b/src/app/settings/options.component.ts index 755d31a7..aad583f5 100644 --- a/src/app/settings/options.component.ts +++ b/src/app/settings/options.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit, } from '@angular/core'; +import { FormControl } from '@angular/forms'; import { ToasterService } from 'angular2-toaster'; @@ -21,15 +22,16 @@ import { Utils } from 'jslib-common/misc/utils'; templateUrl: 'options.component.html', }) export class OptionsComponent implements OnInit { - vaultTimeout: number = null; vaultTimeoutAction: string = 'lock'; disableIcons: boolean; enableGravatars: boolean; enableFullWidth: boolean; locale: string; - vaultTimeouts: any[]; + vaultTimeouts: { name: string; value: number; }[]; localeOptions: any[]; + vaultTimeout: FormControl = new FormControl(null); + private startingLocale: string; constructor(private storageService: StorageService, private stateService: StateService, @@ -63,7 +65,7 @@ export class OptionsComponent implements OnInit { } async ngOnInit() { - this.vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); + this.vaultTimeout.setValue(await this.vaultTimeoutService.getVaultTimeout()); this.vaultTimeoutAction = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); this.disableIcons = await this.storageService.get(ConstantsService.disableFaviconKey); this.enableGravatars = await this.storageService.get('enableGravatars'); @@ -72,8 +74,12 @@ export class OptionsComponent implements OnInit { } async submit() { - await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout != null ? this.vaultTimeout : null, - this.vaultTimeoutAction); + if (!this.vaultTimeout.valid) { + this.toasterService.popAsync('error', null, this.i18nService.t('vaultTimeoutToLarge')); + return; + } + + await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout.value, this.vaultTimeoutAction); await this.storageService.save(ConstantsService.disableFaviconKey, this.disableIcons); await this.stateService.save(ConstantsService.disableFaviconKey, this.disableIcons); await this.storageService.save('enableGravatars', this.enableGravatars); diff --git a/src/app/settings/vault-timeout-input.component.html b/src/app/settings/vault-timeout-input.component.html new file mode 100644 index 00000000..9fd5d631 --- /dev/null +++ b/src/app/settings/vault-timeout-input.component.html @@ -0,0 +1,28 @@ + + {{'vaultTimeoutPolicyInEffect' | i18n : vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes}} + + +
+
+ + + {{'vaultTimeoutDesc' | i18n}} +
+
+ +
+
+ + {{'hours' | i18n }} +
+
+ + {{'minutes' | i18n }} +
+
+
+
diff --git a/src/app/settings/vault-timeout-input.component.ts b/src/app/settings/vault-timeout-input.component.ts new file mode 100644 index 00000000..47335e51 --- /dev/null +++ b/src/app/settings/vault-timeout-input.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { + NG_VALIDATORS, + NG_VALUE_ACCESSOR, +} from '@angular/forms'; + +import { + VaultTimeoutInputComponent as VaultTimeoutInputComponentBase +} from 'jslib-angular/components/settings/vault-timeout-input.component'; + +@Component({ + selector: 'app-vault-timeout-input', + templateUrl: 'vault-timeout-input.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: VaultTimeoutInputComponent, + }, + { + provide: NG_VALIDATORS, + multi: true, + useExisting: VaultTimeoutInputComponent, + }, + ], +}) +export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase { +} diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index c8c46ec2..b396298e 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -4198,5 +4198,39 @@ }, "updateMasterPasswordWarning": { "message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + }, + "maximumVaultTimeout": { + "message": "Vault Timeout" + }, + "maximumVaultTimeoutDesc": { + "message": "Configure a maximum vault timeout for all users." + }, + "maximumVaultTimeoutLabel": { + "message": "Maximum Vault Timeout" + }, + "hours": { + "message": "Hours" + }, + "minutes": { + "message": "Minutes" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies are affecting your vault timeout. Maximum allowed Vault Timeout is $HOURS$ hour(s) and $MINUTES$ minute(s)", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "customVaultTimeout": { + "message": "Custom Vault Timeout" + }, + "vaultTimeoutToLarge": { + "message": "Your vault timeout exceeds the restriction set by your organization." } } diff --git a/src/services/webPlatformUtils.service.ts b/src/services/webPlatformUtils.service.ts index fb321f5b..2a881600 100644 --- a/src/services/webPlatformUtils.service.ts +++ b/src/services/webPlatformUtils.service.ts @@ -82,10 +82,6 @@ export class WebPlatformUtilsService implements PlatformUtilsService { return Promise.resolve(false); } - lockTimeout(): number { - return null; - } - launchUri(uri: string, options?: any): void { const a = document.createElement('a'); a.href = uri;