diff --git a/jslib b/jslib
index 0fa88b44b8..3b3b71d841 160000
--- a/jslib
+++ b/jslib
@@ -1 +1 @@
-Subproject commit 0fa88b44b81730679fedf88a083b4b4b1f5c40ac
+Subproject commit 3b3b71d84192cc195f4626d6294b34d788641215
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 9b14e5afeb..2e745a869e 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -55,9 +55,11 @@ import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/m
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component';
+import { ApiKeyComponent as OrgApiKeyComponent } from './organizations/settings/api-key.component';
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
+import { RotateApiKeyComponent as OrgRotateApiKeyComponent } from './organizations/settings/rotate-api-key.component';
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
import {
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
@@ -274,6 +276,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
+ OrgApiKeyComponent,
OrganizationBillingComponent,
OrganizationSubscriptionComponent,
OrgAttachmentsComponent,
@@ -294,6 +297,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgManageComponent,
OrgPeopleComponent,
OrgReusedPasswordsReportComponent,
+ OrgRotateApiKeyComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
@@ -359,12 +363,14 @@ registerLocaleData(localeZhTw, 'zh-TW');
FolderAddEditComponent,
ModalComponent,
OrgAddEditComponent,
+ OrgApiKeyComponent,
OrgAttachmentsComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
+ OrgRotateApiKeyComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
diff --git a/src/app/organizations/settings/account.component.html b/src/app/organizations/settings/account.component.html
index 22680d8075..62802e23f0 100644
--- a/src/app/organizations/settings/account.component.html
+++ b/src/app/organizations/settings/account.component.html
@@ -31,6 +31,19 @@
{{'save' | i18n}}
+
+
+
+ {{'apiKeyDesc' | i18n}}
+
+ {{'learnMore' | i18n}}
+
+
+
+
+
@@ -51,3 +64,5 @@
+
+
diff --git a/src/app/organizations/settings/account.component.ts b/src/app/organizations/settings/account.component.ts
index d7b613d467..87c85625c6 100644
--- a/src/app/organizations/settings/account.component.ts
+++ b/src/app/organizations/settings/account.component.ts
@@ -18,7 +18,9 @@ import { OrganizationResponse } from 'jslib/models/response/organizationResponse
import { ModalComponent } from '../../modal.component';
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
+import { ApiKeyComponent } from './api-key.component';
import { DeleteOrganizationComponent } from './delete-organization.component';
+import { RotateApiKeyComponent } from './rotate-api-key.component';
@Component({
selector: 'app-org-account',
@@ -27,8 +29,11 @@ import { DeleteOrganizationComponent } from './delete-organization.component';
export class AccountComponent {
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
+ @ViewChild('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef;
+ @ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: ViewContainerRef;
loading = true;
+ canUseApi = false;
org: OrganizationResponse;
formPromise: Promise;
@@ -45,6 +50,7 @@ export class AccountComponent {
this.organizationId = params.organizationId;
try {
this.org = await this.apiService.getOrganization(this.organizationId);
+ this.canUseApi = this.org.useApi;
} catch { }
});
this.loading = false;
@@ -95,4 +101,34 @@ export class AccountComponent {
this.modal = null;
});
}
+
+ viewApiKey() {
+ if (this.modal != null) {
+ this.modal.close();
+ }
+
+ const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
+ this.modal = this.apiKeyModalRef.createComponent(factory).instance;
+ const childComponent = this.modal.show(ApiKeyComponent, this.apiKeyModalRef);
+ childComponent.organizationId = this.organizationId;
+
+ this.modal.onClosed.subscribe(async () => {
+ this.modal = null;
+ });
+ }
+
+ rotateApiKey() {
+ if (this.modal != null) {
+ this.modal.close();
+ }
+
+ const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
+ this.modal = this.rotateApiKeyModalRef.createComponent(factory).instance;
+ const childComponent = this.modal.show(RotateApiKeyComponent, this.rotateApiKeyModalRef);
+ childComponent.organizationId = this.organizationId;
+
+ this.modal.onClosed.subscribe(async () => {
+ this.modal = null;
+ });
+ }
}
diff --git a/src/app/organizations/settings/api-key.component.html b/src/app/organizations/settings/api-key.component.html
new file mode 100644
index 0000000000..9af80677a2
--- /dev/null
+++ b/src/app/organizations/settings/api-key.component.html
@@ -0,0 +1,48 @@
+
diff --git a/src/app/organizations/settings/api-key.component.ts b/src/app/organizations/settings/api-key.component.ts
new file mode 100644
index 0000000000..5e74d0c745
--- /dev/null
+++ b/src/app/organizations/settings/api-key.component.ts
@@ -0,0 +1,50 @@
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToasterService } from 'angular2-toaster';
+import { Angulartics2 } from 'angulartics2';
+
+import { ApiService } from 'jslib/abstractions/api.service';
+import { CryptoService } from 'jslib/abstractions/crypto.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+
+import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
+
+import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
+
+@Component({
+ selector: 'app-api-key',
+ templateUrl: 'api-key.component.html',
+})
+export class ApiKeyComponent {
+ organizationId: string;
+
+ masterPassword: string;
+ formPromise: Promise;
+ clientId: string;
+ clientSecret: string;
+ scope: string;
+
+ constructor(private apiService: ApiService, private i18nService: I18nService,
+ private analytics: Angulartics2, private toasterService: ToasterService,
+ private cryptoService: CryptoService, private router: Router) { }
+
+ async submit() {
+ if (this.masterPassword == null || this.masterPassword === '') {
+ this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('masterPassRequired'));
+ return;
+ }
+
+ const request = new PasswordVerificationRequest();
+ request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
+ try {
+ this.formPromise = this.apiService.postOrganizationApiKey(this.organizationId, request);
+ const response = await this.formPromise;
+ this.clientSecret = response.apiKey;
+ this.clientId = 'organization.' + this.organizationId;
+ this.scope = 'api.organization';
+ this.analytics.eventTrack.next({ action: 'Viewed Organization API Key' });
+ } catch { }
+ }
+}
diff --git a/src/app/organizations/settings/rotate-api-key.component.html b/src/app/organizations/settings/rotate-api-key.component.html
new file mode 100644
index 0000000000..d74bb7949f
--- /dev/null
+++ b/src/app/organizations/settings/rotate-api-key.component.html
@@ -0,0 +1,48 @@
+
diff --git a/src/app/organizations/settings/rotate-api-key.component.ts b/src/app/organizations/settings/rotate-api-key.component.ts
new file mode 100644
index 0000000000..6d1d3878ea
--- /dev/null
+++ b/src/app/organizations/settings/rotate-api-key.component.ts
@@ -0,0 +1,50 @@
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToasterService } from 'angular2-toaster';
+import { Angulartics2 } from 'angulartics2';
+
+import { ApiService } from 'jslib/abstractions/api.service';
+import { CryptoService } from 'jslib/abstractions/crypto.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+
+import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
+
+import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
+
+@Component({
+ selector: 'app-rotate-api-key',
+ templateUrl: 'rotate-api-key.component.html',
+})
+export class RotateApiKeyComponent {
+ organizationId: string;
+
+ masterPassword: string;
+ formPromise: Promise;
+ clientId: string;
+ clientSecret: string;
+ scope: string;
+
+ constructor(private apiService: ApiService, private i18nService: I18nService,
+ private analytics: Angulartics2, private toasterService: ToasterService,
+ private cryptoService: CryptoService, private router: Router) { }
+
+ async submit() {
+ if (this.masterPassword == null || this.masterPassword === '') {
+ this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('masterPassRequired'));
+ return;
+ }
+
+ const request = new PasswordVerificationRequest();
+ request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
+ try {
+ this.formPromise = this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
+ const response = await this.formPromise;
+ this.clientSecret = response.apiKey;
+ this.clientId = 'organization.' + this.organizationId;
+ this.scope = 'api.organization';
+ this.analytics.eventTrack.next({ action: 'Rotated Organization API Key' });
+ } catch { }
+ }
+}
diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json
index 02dabd08d0..f8c9d877df 100644
--- a/src/locales/en/messages.json
+++ b/src/locales/en/messages.json
@@ -2795,5 +2795,27 @@
"free": {
"message": "Free",
"description": "Free, as in 'Free beer'"
+ },
+ "apiKey": {
+ "message": "API Key"
+ },
+ "apiKeyDesc": {
+ "message": "Your API key can be used to authenticate to the Bitwarden public API."
+ },
+ "apiKeyRotateDesc": {
+ "message": "Rotating the API key will invalidate the previous key. You can rotate your API key if you believe that the current key is no longer safe to use."
+ },
+ "apiKeyWarning": {
+ "message": "Your API key has full access to the organization. It should be kept secret."
+ },
+ "oauth2ClientCredentials": {
+ "message": "OAuth 2.0 Client Credentials",
+ "description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
+ },
+ "viewApiKey": {
+ "message": "View API Key"
+ },
+ "rotateApiKey": {
+ "message": "Rotate API Key"
}
}