1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 03:03:43 +00:00

Merge branch 'master' into AC-1423-update-organization-subscription-cloud-page

This commit is contained in:
Shane Melton
2023-06-20 11:44:07 -07:00
262 changed files with 6299 additions and 1142 deletions

View File

@@ -6,7 +6,10 @@ import { Subject, takeUntil } from "rxjs";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import {
EnvironmentService as EnvironmentServiceAbstraction,
Region,
} from "@bitwarden/common/platform/abstractions/environment.service";
@Component({
selector: "environment-selector",
@@ -37,8 +40,8 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
euServerFlagEnabled: boolean;
isOpen = false;
showingModal = false;
selectedEnvironment: ServerEnvironment;
ServerEnvironmentType = ServerEnvironment;
selectedEnvironment: Region;
ServerEnvironmentType = Region;
overlayPostition: ConnectedPosition[] = [
{
originX: "start",
@@ -50,7 +53,7 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
protected componentDestroyed$: Subject<void> = new Subject();
constructor(
protected environmentService: EnvironmentService,
protected environmentService: EnvironmentServiceAbstraction,
protected configService: ConfigServiceAbstraction,
protected router: Router
) {}
@@ -67,18 +70,20 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
this.componentDestroyed$.complete();
}
async toggle(option: ServerEnvironment) {
async toggle(option: Region) {
this.isOpen = !this.isOpen;
if (option === null) {
return;
}
if (option === ServerEnvironment.EU) {
await this.environmentService.setUrls({ base: "https://vault.bitwarden.eu" });
} else if (option === ServerEnvironment.US) {
await this.environmentService.setUrls({ base: "https://vault.bitwarden.com" });
} else if (option === ServerEnvironment.SelfHosted) {
this.updateEnvironmentInfo();
if (option === Region.SelfHosted) {
this.onOpenSelfHostedSettings.emit();
return;
}
await this.environmentService.setRegion(option);
this.updateEnvironmentInfo();
}
@@ -86,14 +91,8 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
this.euServerFlagEnabled = await this.configService.getFeatureFlagBool(
FeatureFlag.DisplayEuEnvironmentFlag
);
const webvaultUrl = this.environmentService.getWebVaultUrl();
if (this.environmentService.isSelfHosted()) {
this.selectedEnvironment = ServerEnvironment.SelfHosted;
} else if (webvaultUrl != null && webvaultUrl.includes("bitwarden.eu")) {
this.selectedEnvironment = ServerEnvironment.EU;
} else {
this.selectedEnvironment = ServerEnvironment.US;
}
this.selectedEnvironment = this.environmentService.selectedRegion;
}
close() {
@@ -101,9 +100,3 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
this.updateEnvironmentInfo();
}
}
enum ServerEnvironment {
US = "US",
EU = "EU",
SelfHosted = "Self-hosted",
}

View File

@@ -1,6 +1,9 @@
import { Directive, EventEmitter, Output } from "@angular/core";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import {
EnvironmentService,
Region,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -25,6 +28,9 @@ export class EnvironmentComponent {
private modalService: ModalService
) {
const urls = this.environmentService.getUrls();
if (this.environmentService.selectedRegion != Region.SelfHosted) {
return;
}
this.baseUrl = urls.base || "";
this.webVaultUrl = urls.webVault || "";

View File

@@ -0,0 +1,137 @@
import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { mock, MockProxy } from "jest-mock-extended";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { IfFeatureDirective } from "./if-feature.directive";
const testBooleanFeature: FeatureFlag = "boolean-feature" as FeatureFlag;
const testStringFeature: FeatureFlag = "string-feature" as FeatureFlag;
const testStringFeatureValue = "test-value";
@Component({
template: `
<div *appIfFeature="testBooleanFeature">
<div data-testid="boolean-content">Hidden behind feature flag</div>
</div>
<div *appIfFeature="stringFeature; value: stringFeatureValue">
<div data-testid="string-content">Hidden behind feature flag</div>
</div>
<div *appIfFeature="missingFlag">
<div data-testid="missing-flag-content">
Hidden behind missing flag. Should not be visible.
</div>
</div>
`,
})
class TestComponent {
testBooleanFeature = testBooleanFeature;
stringFeature = testStringFeature;
stringFeatureValue = testStringFeatureValue;
missingFlag = "missing-flag" as FeatureFlag;
}
describe("IfFeatureDirective", () => {
let fixture: ComponentFixture<TestComponent>;
let content: HTMLElement;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: any) => {
if (typeof flagValue === "boolean") {
mockConfigService.getFeatureFlagBool.mockImplementation((f, defaultValue = false) =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
} else if (typeof flagValue === "string") {
mockConfigService.getFeatureFlagString.mockImplementation((f, defaultValue = "") =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
} else if (typeof flagValue === "number") {
mockConfigService.getFeatureFlagNumber.mockImplementation((f, defaultValue = 0) =>
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
);
}
};
const queryContent = (testId: string) =>
fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement;
beforeEach(async () => {
mockConfigService = mock<ConfigServiceAbstraction>();
await TestBed.configureTestingModule({
declarations: [IfFeatureDirective, TestComponent],
providers: [
{ provide: LogService, useValue: mock<LogService>() },
{
provide: ConfigServiceAbstraction,
useValue: mockConfigService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
});
it("renders content when the feature flag is enabled", async () => {
mockConfigFlagValue(testBooleanFeature, true);
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("boolean-content");
expect(content).toBeDefined();
});
it("renders content when the feature flag value matches the provided value", async () => {
mockConfigFlagValue(testStringFeature, testStringFeatureValue);
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("string-content");
expect(content).toBeDefined();
});
it("hides content when the feature flag is disabled", async () => {
mockConfigFlagValue(testBooleanFeature, false);
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("boolean-content");
expect(content).toBeUndefined();
});
it("hides content when the feature flag value does not match the provided value", async () => {
mockConfigFlagValue(testStringFeature, "wrong-value");
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("string-content");
expect(content).toBeUndefined();
});
it("hides content when the feature flag is missing", async () => {
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("missing-flag-content");
expect(content).toBeUndefined();
});
it("hides content when the directive throws an unexpected exception", async () => {
mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error"));
fixture.detectChanges();
await fixture.whenStable();
content = queryContent("boolean-content");
expect(content).toBeUndefined();
});
});

View File

@@ -0,0 +1,67 @@
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
// Replace this with a type safe lookup of the feature flag values in PM-2282
type FlagValue = boolean | number | string;
/**
* Directive that conditionally renders the element when the feature flag is enabled and/or
* matches the value specified by {@link appIfFeatureValue}.
*
* When a feature flag is not found in the config service, the element is hidden.
*/
@Directive({
selector: "[appIfFeature]",
})
export class IfFeatureDirective implements OnInit {
/**
* The feature flag to check.
*/
@Input() appIfFeature: FeatureFlag;
/**
* Optional value to compare against the value of the feature flag in the config service.
* @default true
*/
@Input() appIfFeatureValue: FlagValue = true;
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private configService: ConfigServiceAbstraction,
private logService: LogService
) {}
async ngOnInit() {
try {
let flagValue: FlagValue;
if (typeof this.appIfFeatureValue === "boolean") {
flagValue = await this.configService.getFeatureFlagBool(this.appIfFeature);
} else if (typeof this.appIfFeatureValue === "number") {
flagValue = await this.configService.getFeatureFlagNumber(this.appIfFeature);
} else if (typeof this.appIfFeatureValue === "string") {
flagValue = await this.configService.getFeatureFlagString(this.appIfFeature);
}
if (this.appIfFeatureValue === flagValue) {
if (!this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
}
} else {
this.viewContainer.clear();
this.hasView = false;
}
} catch (e) {
this.logService.error(e);
this.viewContainer.clear();
this.hasView = false;
}
}
}

View File

@@ -11,6 +11,7 @@ import { AutofocusDirective } from "./directives/autofocus.directive";
import { BoxRowDirective } from "./directives/box-row.directive";
import { CopyClickDirective } from "./directives/copy-click.directive";
import { FallbackSrcDirective } from "./directives/fallback-src.directive";
import { IfFeatureDirective } from "./directives/if-feature.directive";
import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive";
import { InputVerbatimDirective } from "./directives/input-verbatim.directive";
import { LaunchClickDirective } from "./directives/launch-click.directive";
@@ -25,6 +26,7 @@ import { SearchPipe } from "./pipes/search.pipe";
import { UserNamePipe } from "./pipes/user-name.pipe";
import { UserTypePipe } from "./pipes/user-type.pipe";
import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe";
import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe";
import { I18nPipe } from "./platform/pipes/i18n.pipe";
import { PasswordStrengthComponent } from "./shared/components/password-strength/password-strength.component";
import { ExportScopeCalloutComponent } from "./tools/export/components/export-scope-callout.component";
@@ -68,6 +70,8 @@ import { IconComponent } from "./vault/components/icon.component";
UserNamePipe,
PasswordStrengthComponent,
UserTypePipe,
IfFeatureDirective,
FingerprintPipe,
],
exports: [
A11yInvalidDirective,
@@ -97,7 +101,17 @@ import { IconComponent } from "./vault/components/icon.component";
UserNamePipe,
PasswordStrengthComponent,
UserTypePipe,
IfFeatureDirective,
FingerprintPipe,
],
providers: [
CreditCardNumberPipe,
DatePipe,
I18nPipe,
SearchPipe,
UserNamePipe,
UserTypePipe,
FingerprintPipe,
],
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe, UserTypePipe],
})
export class JslibModule {}

View File

@@ -0,0 +1,32 @@
import { Pipe } from "@angular/core";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@Pipe({
name: "fingerprint",
})
export class FingerprintPipe {
constructor(private cryptoService: CryptoService) {}
async transform(publicKey: string | Uint8Array, fingerprintMaterial: string): Promise<string> {
try {
if (typeof publicKey === "string") {
publicKey = Utils.fromB64ToArray(publicKey);
}
const fingerprint = await this.cryptoService.getFingerprint(
fingerprintMaterial,
publicKey.buffer
);
if (fingerprint != null) {
return fingerprint.join("-");
}
return "";
} catch {
return "";
}
}
}

View File

@@ -14,6 +14,7 @@ export class OrganizationUserResponse extends BaseResponse {
accessSecretsManager: boolean;
permissions: PermissionsApi;
resetPasswordEnrolled: boolean;
hasMasterPassword: boolean;
collections: SelectionReadOnlyResponse[] = [];
groups: string[] = [];
@@ -28,6 +29,7 @@ export class OrganizationUserResponse extends BaseResponse {
this.accessAll = this.getResponseProperty("AccessAll");
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
const collections = this.getResponseProperty("Collections");
if (collections != null) {

View File

@@ -24,7 +24,7 @@ export abstract class CryptoService {
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
getPublicKey: () => Promise<ArrayBuffer>;
getPrivateKey: () => Promise<ArrayBuffer>;
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
getFingerprint: (fingerprintMaterial: string, publicKey?: ArrayBuffer) => Promise<string[]>;
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;

View File

@@ -17,8 +17,17 @@ export type PayPalConfig = {
buttonAction?: string;
};
export enum Region {
US = "US",
EU = "EU",
SelfHosted = "Self-hosted",
}
export abstract class EnvironmentService {
urls: Observable<void>;
usUrls: Urls;
euUrls: Urls;
selectedRegion?: Region;
hasBaseUrl: () => boolean;
getNotificationsUrl: () => string;
@@ -32,8 +41,10 @@ export abstract class EnvironmentService {
getScimUrl: () => string;
setUrlsFromStorage: () => Promise<void>;
setUrls: (urls: Urls) => Promise<Urls>;
setRegion: (region: Region) => Promise<void>;
getUrls: () => Urls;
isCloud: () => boolean;
isEmpty: () => boolean;
/**
* @remarks For desktop and browser use only.
* For web, use PlatformUtilsService.isSelfHost()

View File

@@ -263,6 +263,8 @@ export abstract class StateService<T extends Account = Account> {
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
getRegion: (options?: StorageOptions) => Promise<string>;
setRegion: (value: string, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;

View File

@@ -233,6 +233,7 @@ export class AccountSettings {
approveLoginRequests?: boolean;
avatarColor?: string;
activateAutoFillOnPageLoadFromPolicy?: boolean;
region?: string;
smOnboardingTasks?: Record<string, Record<string, boolean>>;
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {

View File

@@ -36,4 +36,5 @@ export class GlobalState {
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean;
region?: string;
}

View File

@@ -204,7 +204,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return privateKey;
}
async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise<string[]> {
async getFingerprint(fingerprintMaterial: string, publicKey?: ArrayBuffer): Promise<string[]> {
if (publicKey == null) {
publicKey = await this.getPublicKey();
}
@@ -214,7 +214,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
keyFingerprint,
userId,
fingerprintMaterial,
32,
"sha256"
);

View File

@@ -3,6 +3,7 @@ import { concatMap, Observable, Subject } from "rxjs";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import {
EnvironmentService as EnvironmentServiceAbstraction,
Region,
Urls,
} from "../abstractions/environment.service";
import { StateService } from "../abstractions/state.service";
@@ -10,6 +11,7 @@ import { StateService } from "../abstractions/state.service";
export class EnvironmentService implements EnvironmentServiceAbstraction {
private readonly urlsSubject = new Subject<void>();
urls: Observable<void> = this.urlsSubject.asObservable();
selectedRegion?: Region;
protected baseUrl: string;
protected webVaultUrl: string;
@@ -21,6 +23,28 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
private keyConnectorUrl: string;
private scimUrl: string = null;
readonly usUrls: Urls = {
base: null,
api: "https://api.bitwarden.com",
identity: "https://identity.bitwarden.com",
icons: "https://icons.bitwarden.net",
webVault: "https://vault.bitwarden.com",
notifications: "https://notifications.bitwarden.com",
events: "https://events.bitwarden.com",
scim: "https://scim.bitwarden.com/v2",
};
readonly euUrls: Urls = {
base: null,
api: "https://api.bitwarden.eu",
identity: "https://identity.bitwarden.eu",
icons: "https://icons.bitwarden.eu",
webVault: "https://vault.bitwarden.eu",
notifications: "https://notifications.bitwarden.eu",
events: "https://events.bitwarden.eu",
scim: "https://scim.bitwarden.eu/v2",
};
constructor(private stateService: StateService) {
this.stateService.activeAccount$
.pipe(
@@ -127,20 +151,42 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
}
async setUrlsFromStorage(): Promise<void> {
const urls: any = await this.stateService.getEnvironmentUrls();
const region = await this.stateService.getRegion();
const savedUrls = await this.stateService.getEnvironmentUrls();
const envUrls = new EnvironmentUrls();
this.baseUrl = envUrls.base = urls.base;
this.webVaultUrl = urls.webVault;
this.apiUrl = envUrls.api = urls.api;
this.identityUrl = envUrls.identity = urls.identity;
this.iconsUrl = urls.icons;
this.notificationsUrl = urls.notifications;
this.eventsUrl = envUrls.events = urls.events;
this.keyConnectorUrl = urls.keyConnector;
// scimUrl is not saved to storage
// fix environment urls for old users
if (savedUrls.base === "https://vault.bitwarden.com") {
this.setRegion(Region.US);
return;
}
if (savedUrls.base === "https://vault.bitwarden.eu") {
this.setRegion(Region.EU);
return;
}
this.urlsSubject.next();
switch (region) {
case Region.EU:
this.setRegion(Region.EU);
return;
case Region.US:
this.setRegion(Region.US);
return;
case Region.SelfHosted:
default:
this.baseUrl = envUrls.base = savedUrls.base;
this.webVaultUrl = savedUrls.webVault;
this.apiUrl = envUrls.api = savedUrls.api;
this.identityUrl = envUrls.identity = savedUrls.identity;
this.iconsUrl = savedUrls.icons;
this.notificationsUrl = savedUrls.notifications;
this.eventsUrl = envUrls.events = savedUrls.events;
this.keyConnectorUrl = savedUrls.keyConnector;
// scimUrl is not saved to storage
this.urlsSubject.next();
this.setRegion(Region.SelfHosted);
break;
}
}
async setUrls(urls: Urls): Promise<Urls> {
@@ -178,6 +224,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
this.keyConnectorUrl = urls.keyConnector;
this.scimUrl = urls.scim;
await this.setRegion(Region.SelfHosted);
this.urlsSubject.next();
return urls;
@@ -197,6 +245,52 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
};
}
isEmpty(): boolean {
return (
this.baseUrl == null &&
this.webVaultUrl == null &&
this.apiUrl == null &&
this.identityUrl == null &&
this.iconsUrl == null &&
this.notificationsUrl == null &&
this.eventsUrl == null
);
}
async setRegion(region: Region) {
this.selectedRegion = region;
await this.stateService.setRegion(region);
switch (region) {
case Region.EU:
this.setUrlsInternal(this.euUrls);
break;
case Region.US:
this.setUrlsInternal(this.usUrls);
break;
case Region.SelfHosted:
// if user saves with empty fields, default to US
if (this.isEmpty()) {
this.setRegion(Region.US);
}
break;
}
}
private setUrlsInternal(urls: Urls) {
this.baseUrl = this.formatUrl(urls.base);
this.webVaultUrl = this.formatUrl(urls.webVault);
this.apiUrl = this.formatUrl(urls.api);
this.identityUrl = this.formatUrl(urls.identity);
this.iconsUrl = this.formatUrl(urls.icons);
this.notificationsUrl = this.formatUrl(urls.notifications);
this.eventsUrl = this.formatUrl(urls.events);
this.keyConnectorUrl = this.formatUrl(urls.keyConnector);
// scimUrl cannot be cleared
this.scimUrl = this.formatUrl(urls.scim) ?? this.scimUrl;
this.urlsSubject.next();
}
private formatUrl(url: string): string {
if (url == null || url === "") {
return null;
@@ -211,9 +305,12 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
}
isCloud(): boolean {
return ["https://api.bitwarden.com", "https://vault.bitwarden.com/api"].includes(
this.getApiUrl()
);
return [
"https://api.bitwarden.com",
"https://vault.bitwarden.com/api",
"https://api.bitwarden.eu",
"https://vault.bitwarden.eu/api",
].includes(this.getApiUrl());
}
isSelfHosted(): boolean {

View File

@@ -1598,6 +1598,28 @@ export class StateService<
);
}
async getRegion(options?: StorageOptions): Promise<string> {
if ((await this.state())?.activeUserId == null) {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getGlobals(options)).region ?? null;
}
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getAccount(options))?.settings?.region ?? null;
}
async setRegion(value: string, options?: StorageOptions): Promise<void> {
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
// This is to allow setting region values before an account is active, while still allowing individual accounts to have their own region.
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
globals.region = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))

View File

@@ -18,6 +18,7 @@ export class AnonAddyForwarder implements Forwarder {
headers: new Headers({
Authorization: "Bearer " + options.apiKey,
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
}),
};
const url = "https://app.anonaddy.com/api/v1/aliases";

View File

@@ -1,4 +1,4 @@
import { Component } from "@angular/core";
import { Component, Input } from "@angular/core";
import { Icons } from "..";
@@ -10,5 +10,5 @@ import { Icons } from "..";
templateUrl: "./no-items.component.html",
})
export class NoItemsComponent {
protected icon = Icons.Search;
@Input() icon = Icons.Search;
}

View File

@@ -1,10 +1,10 @@
import { HostBinding, Directive } from "@angular/core";
import { Directive, HostBinding } from "@angular/core";
@Directive({
selector: "th[bitCell], td[bitCell]",
})
export class CellDirective {
@HostBinding("class") get classList() {
return ["tw-p-3", "tw-align-middle"];
return ["tw-p-3"];
}
}

View File

@@ -4,7 +4,7 @@ import { Directive, HostBinding, Input } from "@angular/core";
selector: "tr[bitRow]",
})
export class RowDirective {
@Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "baseline";
@Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "middle";
get alignmentClass(): string {
switch (this.alignContent) {

View File

@@ -1,5 +1,5 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { countries } from "../form/countries";
@@ -62,7 +62,7 @@ export const Default: Story = {
`,
}),
args: {
alignRowContent: "baseline",
alignRowContent: "middle",
},
};