1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 16:43:27 +00:00

Merge branch 'main' into autofill/pm-12763-modify-autofill-animation

This commit is contained in:
Cesar Gonzalez
2024-10-16 15:27:15 -05:00
committed by GitHub
29 changed files with 458 additions and 58 deletions

View File

@@ -34,7 +34,7 @@ export class HintComponent extends BaseHintComponent {
toastService,
);
super.onSuccessfulSubmit = async () => {
this.onSuccessfulSubmit = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);

View File

@@ -105,7 +105,7 @@ export class LockComponent extends BaseLockComponent implements OnInit {
this.successRoute = "/tabs/current";
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
super.onSuccessfulSubmit = async () => {
this.onSuccessfulSubmit = async () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@@ -74,7 +74,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
loginStrategyService,
toastService,
);
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
};
}

View File

@@ -78,10 +78,10 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
registerRouteService,
toastService,
);
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
};
super.successRoute = "/tabs/vault";
this.successRoute = "/tabs/vault";
this.showPasswordless = flagEnabled("showPasswordless");
}

View File

@@ -79,7 +79,7 @@ export class SsoComponent extends BaseSsoComponent {
});
this.clientId = "browser";
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
@@ -92,13 +92,13 @@ export class SsoComponent extends BaseSsoComponent {
this.win.close();
};
super.onSuccessfulLoginTde = async () => {
this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
super.onSuccessfulLoginTdeNavigate = async () => {
this.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
}

View File

@@ -118,7 +118,7 @@ export class TwoFactorAuthComponent
win,
toastService,
);
super.onSuccessfulLoginTdeNavigate = async () => {
this.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
@@ -131,7 +131,7 @@ export class TwoFactorAuthComponent
// WebAuthn fallback response
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
this.token = this.route.snapshot.paramMap.get("webAuthnResponse");
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.syncService.fullSync(true);

View File

@@ -87,23 +87,23 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit
accountService,
toastService,
);
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
super.onSuccessfulLoginTde = async () => {
this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
super.onSuccessfulLoginTdeNavigate = async () => {
this.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
super.successRoute = "/tabs/vault";
this.successRoute = "/tabs/vault";
// FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe
this.webAuthnNewTab = true;
}
@@ -113,7 +113,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit
// WebAuthn fallback response
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
this.token = this.route.snapshot.paramMap.get("webAuthnResponse");
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.syncService.fullSync(true);
@@ -155,7 +155,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.sso === "true") {
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
// This is not awaited so we don't pause the application while the sync is happening.
// This call is executed by the service that lives in the background script so it will continue
// the sync even if this tab closes.

View File

@@ -1158,7 +1158,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
if (
this.showInlineMenuIdentities &&
this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm(
autofillFieldData,
pageDetails,

View File

@@ -80,7 +80,7 @@
"papaparse": "5.4.1",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
"tldts": "6.1.51",
"tldts": "6.1.52",
"zxcvbn": "4.4.2"
}
}

View File

@@ -83,7 +83,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
toastService,
);
super.onSuccessfulLogin = () => {
this.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
}

View File

@@ -99,7 +99,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
registerRouteService,
toastService,
);
super.onSuccessfulLogin = () => {
this.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
}

View File

@@ -65,13 +65,13 @@ export class SsoComponent extends BaseSsoComponent {
accountService,
toastService,
);
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
super.onSuccessfulLoginTde = async () => {
this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);

View File

@@ -89,13 +89,13 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
accountService,
toastService,
);
super.onSuccessfulLogin = async () => {
this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
super.onSuccessfulLoginTde = async () => {
this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);

View File

@@ -2,15 +2,7 @@
{{ "personalOwnershipExemption" | i18n }}
</bit-callout>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>
<bit-form-control>
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
<bit-label>{{ "turnOn" | i18n }}</bit-label>
</bit-form-control>

View File

@@ -71,7 +71,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn
dialogService,
toastService,
);
super.modifyRegisterRequest = async (request: RegisterRequest) => {
this.modifyRegisterRequest = async (request: RegisterRequest) => {
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
// Org user id and token are included here only for validation and two factor purposes.
const orgInvite = await acceptOrgInviteService.getOrganizationInvite();

View File

@@ -1,7 +1,6 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { unauthGuardFn } from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -11,7 +10,7 @@ const routes: Routes = [
{
path: "",
component: AccessIntelligenceComponent,
canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence), unauthGuardFn()],
canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)],
data: {
titleId: "accessIntelligence",
},

View File

@@ -1,6 +1,9 @@
<app-header></app-header>
<bit-tab-group [(selectedIndex)]="tabIndex">
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
<bit-tab label="Raw Data">
<tools-password-health></tools-password-health>
</bit-tab>
<!-- <bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
<h2 bitTypography="h2">{{ "allApplications" | i18n }}</h2>
<tools-application-table></tools-application-table>
</bit-tab>
@@ -19,5 +22,5 @@
</ng-template>
<h2 bitTypography="h2">{{ "notifiedMembers" | i18n }}</h2>
<tools-notified-members-table></tools-notified-members-table>
</bit-tab>
</bit-tab> -->
</bit-tab-group>

View File

@@ -11,6 +11,7 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { ApplicationTableComponent } from "./application-table.component";
import { NotifiedMembersTableComponent } from "./notified-members-table.component";
import { PasswordHealthComponent } from "./password-health.component";
export enum AccessIntelligenceTabType {
AllApps = 0,
@@ -26,6 +27,7 @@ export enum AccessIntelligenceTabType {
CommonModule,
JslibModule,
HeaderModule,
PasswordHealthComponent,
NotifiedMembersTableComponent,
TabsModule,
],

View File

@@ -0,0 +1,57 @@
<bit-container>
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!loading">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>
<app-vault-icon [cipher]="r"></app-vault-icon>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
</tr>
</ng-template>
</bit-table>
</div>
</bit-container>

View File

@@ -0,0 +1,114 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, convertToParamMap } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TableModule } from "@bitwarden/components";
import { TableBodyDirective } from "@bitwarden/components/src/table/table.component";
import { LooseComponentsModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
// eslint-disable-next-line no-restricted-imports
import { cipherData } from "../reports/pages/reports-ciphers.mock";
import { PasswordHealthComponent } from "./password-health.component";
describe("PasswordHealthComponent", () => {
let component: PasswordHealthComponent;
let fixture: ComponentFixture<PasswordHealthComponent>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let organizationService: MockProxy<OrganizationService>;
let cipherServiceMock: MockProxy<CipherService>;
let auditServiceMock: MockProxy<AuditService>;
const activeRouteParams = convertToParamMap({ organizationId: "orgId" });
beforeEach(async () => {
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
auditServiceMock = mock<AuditService>();
organizationService = mock<OrganizationService>({
get: jest.fn().mockResolvedValue({ id: "orgId" } as Organization),
});
cipherServiceMock = mock<CipherService>({
getAllFromApiForOrganization: jest.fn().mockResolvedValue(cipherData),
});
await TestBed.configureTestingModule({
imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule],
declarations: [TableBodyDirective],
providers: [
{ provide: CipherService, useValue: cipherServiceMock },
{ provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService },
{ provide: OrganizationService, useValue: organizationService },
{ provide: I18nService, useValue: mock<I18nService>() },
{ provide: AuditService, useValue: auditServiceMock },
{
provide: ActivatedRoute,
useValue: {
paramMap: of(activeRouteParams),
url: of([]),
},
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PasswordHealthComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should initialize component", () => {
expect(component).toBeTruthy();
});
it("should populate reportCiphers with ciphers that have password issues", async () => {
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 1 } as any);
auditServiceMock.passwordLeaked.mockResolvedValue(5);
await component.setCiphers();
const cipherIds = component.reportCiphers.map((c) => c.id);
expect(cipherIds).toEqual([
"cbea34a8-bde4-46ad-9d19-b05001228ab1",
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
]);
expect(component.reportCiphers.length).toEqual(3);
});
it("should correctly populate passwordStrengthMap", async () => {
passwordStrengthService.getPasswordStrength.mockImplementation((password) => {
let score = 0;
if (password === "123") {
score = 1;
} else {
score = 4;
}
return { score } as any;
});
auditServiceMock.passwordLeaked.mockResolvedValue(0);
await component.setCiphers();
expect(component.passwordStrengthMap.size).toBeGreaterThan(0);
expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([
"veryWeak",
"danger",
]);
expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([
"veryWeak",
"danger",
]);
});
});

View File

@@ -0,0 +1,229 @@
import { CommonModule } from "@angular/common";
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { from, map, switchMap, tap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
BadgeVariant,
ContainerComponent,
TableDataSource,
TableModule,
} from "@bitwarden/components";
// eslint-disable-next-line no-restricted-imports
import { HeaderModule } from "../../layouts/header/header.module";
// eslint-disable-next-line no-restricted-imports
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
// eslint-disable-next-line no-restricted-imports
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
@Component({
standalone: true,
selector: "tools-password-health",
templateUrl: "password-health.component.html",
imports: [
BadgeModule,
OrganizationBadgeModule,
CommonModule,
ContainerComponent,
PipesModule,
JslibModule,
HeaderModule,
TableModule,
],
})
export class PasswordHealthComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
weakPasswordCiphers: CipherView[] = [];
passwordUseMap = new Map<string, number>();
exposedPasswordMap = new Map<string, number>();
dataSource = new TableDataSource<CipherView>();
reportCiphers: CipherView[] = [];
reportCipherIds: string[] = [];
organization: Organization;
loading = true;
private destroyRef = inject(DestroyRef);
constructor(
protected cipherService: CipherService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected organizationService: OrganizationService,
protected auditService: AuditService,
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
) {}
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map((params) => params.get("organizationId")),
switchMap((organizationId) => {
return from(this.organizationService.get(organizationId));
}),
tap((organization) => {
this.organization = organization;
}),
switchMap(() => from(this.setCiphers())),
)
.subscribe();
}
async setCiphers() {
const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
allCiphers.forEach(async (cipher) => {
this.findWeakPassword(cipher);
this.findReusedPassword(cipher);
await this.findExposedPassword(cipher);
});
this.dataSource.data = this.reportCiphers;
this.loading = false;
// const reportIssues = allCiphers.map((c) => {
// if (this.passwordStrengthMap.has(c.id)) {
// return c;
// }
// if (this.passwordUseMap.has(c.id)) {
// return c;
// }
// if (this.exposedPasswordMap.has(c.id)) {
// return c;
// }
// });
}
protected checkForExistingCipher(ciph: CipherView) {
if (!this.reportCipherIds.includes(ciph.id)) {
this.reportCipherIds.push(ciph.id);
this.reportCiphers.push(ciph);
}
}
protected async findExposedPassword(cipher: CipherView) {
const { type, login, isDeleted, edit, viewPassword, id } = cipher;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}
const exposedCount = await this.auditService.passwordLeaked(login.password);
if (exposedCount > 0) {
this.exposedPasswordMap.set(id, exposedCount);
this.checkForExistingCipher(cipher);
}
}
protected findReusedPassword(cipher: CipherView) {
const { type, login, isDeleted, edit, viewPassword } = cipher;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}
if (this.passwordUseMap.has(login.password)) {
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1);
} else {
this.passwordUseMap.set(login.password, 1);
}
this.checkForExistingCipher(cipher);
}
protected findWeakPassword(cipher: CipherView): void {
const { type, login, isDeleted, edit, viewPassword } = cipher;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}
const hasUserName = this.isUserNameNotEmpty(cipher);
let userInput: string[] = [];
if (hasUserName) {
const atPosition = login.username.indexOf("@");
if (atPosition > -1) {
userInput = userInput
.concat(
login.username
.substring(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/),
)
.filter((i) => i.length >= 3);
} else {
userInput = login.username
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
.filter((i) => i.length >= 3);
}
}
const { score } = this.passwordStrengthService.getPasswordStrength(
login.password,
null,
userInput.length > 0 ? userInput : null,
);
if (score != null && score <= 2) {
this.passwordStrengthMap.set(cipher.id, this.scoreKey(score));
this.checkForExistingCipher(cipher);
}
}
private isUserNameNotEmpty(c: CipherView): boolean {
return !Utils.isNullOrWhitespace(c.login.username);
}
private scoreKey(score: number): [string, BadgeVariant] {
switch (score) {
case 4:
return ["strong", "success"];
case 3:
return ["good", "primary"];
case 2:
return ["weak", "warning"];
default:
return ["veryWeak", "danger"];
}
}
}

View File

@@ -288,6 +288,7 @@ function createCollectionView(i: number): CollectionAdminView {
view.id = `collection-${i}`;
view.name = `Collection ${i}`;
view.organizationId = organization?.id;
view.manage = true;
if (group !== undefined) {
view.groups = [

View File

@@ -105,6 +105,11 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
copyDnsTxt(): void {
this.orgDomainService.copyDnsTxt(this.txtCtrl.value);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")),
});
}
// End Form methods

View File

@@ -101,6 +101,11 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
copyDnsTxt(dnsTxt: string): void {
this.orgDomainService.copyDnsTxt(dnsTxt);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")),
});
}
async verifyDomain(orgDomainId: string, domainName: string): Promise<void> {

View File

@@ -180,6 +180,5 @@ describe("Org Domain Service", () => {
it("copyDnsTxt copies DNS TXT to clipboard and shows toast", () => {
orgDomainService.copyDnsTxt("fakeTxt");
expect(jest.spyOn(platformUtilService, "copyToClipboard")).toHaveBeenCalled();
expect(jest.spyOn(platformUtilService, "showToast")).toHaveBeenCalled();
});
});

View File

@@ -23,11 +23,6 @@ export class OrgDomainService implements OrgDomainInternalServiceAbstraction {
copyDnsTxt(dnsTxt: string): void {
this.platformUtilsService.copyToClipboard(dnsTxt);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")),
);
}
upsert(orgDomains: OrganizationDomainResponse[]): void {

View File

@@ -43,7 +43,7 @@
></button>
<bit-hint>{{ "sendPasswordDescV2" | i18n }}</bit-hint>
</bit-form-field>
<bit-form-control *ngIf="!disableHideEmail || originalSendView.hideEmail">
<bit-form-control *ngIf="!disableHideEmail || originalSendView?.hideEmail">
<input
[disabled]="disableHideEmail && !sendOptionsForm.get('hideEmail').value"
bitCheckbox

18
package-lock.json generated
View File

@@ -68,7 +68,7 @@
"qrious": "4.0.2",
"rxjs": "7.8.1",
"tabbable": "6.2.0",
"tldts": "6.1.51",
"tldts": "6.1.52",
"utf-8-validate": "6.0.4",
"zone.js": "0.13.3",
"zxcvbn": "4.4.2"
@@ -225,7 +225,7 @@
"papaparse": "5.4.1",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
"tldts": "6.1.51",
"tldts": "6.1.52",
"zxcvbn": "4.4.2"
},
"bin": {
@@ -36519,21 +36519,21 @@
}
},
"node_modules/tldts": {
"version": "6.1.51",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.51.tgz",
"integrity": "sha512-33lfQoL0JsDogIbZ8fgRyvv77GnRtwkNE/MOKocwUgPO1WrSfsq7+vQRKxRQZai5zd+zg97Iv9fpFQSzHyWdLA==",
"version": "6.1.52",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.52.tgz",
"integrity": "sha512-fgrDJXDjbAverY6XnIt0lNfv8A0cf7maTEaZxNykLGsLG7XP+5xhjBTrt/ieAsFjAlZ+G5nmXomLcZDkxXnDzw==",
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.51"
"tldts-core": "^6.1.52"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.51",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.51.tgz",
"integrity": "sha512-bu9oCYYWC1iRjx+3UnAjqCsfrWNZV1ghNQf49b3w5xE8J/tNShHTzp5syWJfwGH+pxUgTTLUnzHnfuydW7wmbg==",
"version": "6.1.52",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.52.tgz",
"integrity": "sha512-j4OxQI5rc1Ve/4m/9o2WhWSC4jGc4uVbCINdOEJRAraCi0YqTqgMcxUx7DbmuP0G3PCixoof/RZB0Q5Kh9tagw==",
"license": "MIT"
},
"node_modules/tmp": {

View File

@@ -202,7 +202,7 @@
"qrious": "4.0.2",
"rxjs": "7.8.1",
"tabbable": "6.2.0",
"tldts": "6.1.51",
"tldts": "6.1.52",
"utf-8-validate": "6.0.4",
"zone.js": "0.13.3",
"zxcvbn": "4.4.2"