diff --git a/libs/common/src/tools/generator/key-definition.spec.ts b/libs/common/src/tools/generator/key-definition.spec.ts index 6ee9288820..735377a5ba 100644 --- a/libs/common/src/tools/generator/key-definition.spec.ts +++ b/libs/common/src/tools/generator/key-definition.spec.ts @@ -5,6 +5,12 @@ import { SUBADDRESS_SETTINGS, PASSPHRASE_SETTINGS, PASSWORD_SETTINGS, + SIMPLE_LOGIN_FORWARDER, + FORWARD_EMAIL_FORWARDER, + FIREFOX_RELAY_FORWARDER, + FASTMAIL_FORWARDER, + DUCK_DUCK_GO_FORWARDER, + ADDY_IO_FORWARDER, } from "./key-definitions"; describe("Key definitions", () => { @@ -48,6 +54,54 @@ describe("Key definitions", () => { }); }); + describe("ADDY_IO_FORWARDER", () => { + it("should pass through deserialization", () => { + const value: any = {}; + const result = ADDY_IO_FORWARDER.deserializer(value); + expect(result).toBe(value); + }); + }); + + describe("DUCK_DUCK_GO_FORWARDER", () => { + it("should pass through deserialization", () => { + const value: any = {}; + const result = DUCK_DUCK_GO_FORWARDER.deserializer(value); + expect(result).toBe(value); + }); + }); + + describe("FASTMAIL_FORWARDER", () => { + it("should pass through deserialization", () => { + const value: any = {}; + const result = FASTMAIL_FORWARDER.deserializer(value); + expect(result).toBe(value); + }); + }); + + describe("FIREFOX_RELAY_FORWARDER", () => { + it("should pass through deserialization", () => { + const value: any = {}; + const result = FIREFOX_RELAY_FORWARDER.deserializer(value); + expect(result).toBe(value); + }); + }); + + describe("FORWARD_EMAIL_FORWARDER", () => { + it("should pass through deserialization", () => { + const value: any = {}; + const result = FORWARD_EMAIL_FORWARDER.deserializer(value); + expect(result).toBe(value); + }); + }); + + describe("SIMPLE_LOGIN_FORWARDER", () => { + it("should pass through deserialization", () => { + const value: any = {}; + const result = SIMPLE_LOGIN_FORWARDER.deserializer(value); + expect(result).toBe(value); + }); + }); + describe("ENCRYPTED_HISTORY", () => { it("should pass through deserialization", () => { const value = {}; diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index fc51e430dd..bb7c4e8a08 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -5,6 +5,12 @@ import { GeneratedPasswordHistory } from "./password/generated-password-history" import { PasswordGenerationOptions } from "./password/password-generation-options"; import { CatchallGenerationOptions } from "./username/catchall-generator-options"; import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options"; +import { + ApiOptions, + EmailDomainOptions, + EmailPrefixOptions, + SelfHostedApiOptions, +} from "./username/options/forwarder-options"; import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; /** plaintext password generation options */ @@ -52,6 +58,54 @@ export const SUBADDRESS_SETTINGS = new KeyDefinition( + GENERATOR_DISK, + "addyIoForwarder", + { + deserializer: (value) => value, + }, +); + +export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition( + GENERATOR_DISK, + "duckDuckGoForwarder", + { + deserializer: (value) => value, + }, +); + +export const FASTMAIL_FORWARDER = new KeyDefinition( + GENERATOR_DISK, + "fastmailForwarder", + { + deserializer: (value) => value, + }, +); + +export const FIREFOX_RELAY_FORWARDER = new KeyDefinition( + GENERATOR_DISK, + "firefoxRelayForwarder", + { + deserializer: (value) => value, + }, +); + +export const FORWARD_EMAIL_FORWARDER = new KeyDefinition( + GENERATOR_DISK, + "forwardEmailForwarder", + { + deserializer: (value) => value, + }, +); + +export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition( + GENERATOR_DISK, + "simpleLoginForwarder", + { + deserializer: (value) => value, + }, +); + /** encrypted password generation history */ export const ENCRYPTED_HISTORY = new KeyDefinition( GENERATOR_DISK, diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts new file mode 100644 index 0000000000..96a7bca2b1 --- /dev/null +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts @@ -0,0 +1,73 @@ +import { mock } from "jest-mock-extended"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { StateProvider } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { DUCK_DUCK_GO_FORWARDER } from "../key-definitions"; +import { SecretState } from "../state/secret-state"; + +import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy"; +import { ApiOptions } from "./options/forwarder-options"; + +class TestForwarder extends ForwarderGeneratorStrategy { + constructor( + encryptService: EncryptService, + keyService: CryptoService, + stateProvider: StateProvider, + ) { + super(encryptService, keyService, stateProvider); + } + + get key() { + // arbitrary. + return DUCK_DUCK_GO_FORWARDER; + } +} + +const SomeUser = "some user" as UserId; +const AnotherUser = "another user" as UserId; + +describe("ForwarderGeneratorStrategy", () => { + const encryptService = mock(); + const keyService = mock(); + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + + describe("durableState", () => { + it("constructs a secret state", () => { + const strategy = new TestForwarder(encryptService, keyService, stateProvider); + + const result = strategy.durableState(SomeUser); + + expect(result).toBeInstanceOf(SecretState); + }); + + it("returns the same secret state for a single user", () => { + const strategy = new TestForwarder(encryptService, keyService, stateProvider); + + const firstResult = strategy.durableState(SomeUser); + const secondResult = strategy.durableState(SomeUser); + + expect(firstResult).toBe(secondResult); + }); + + it("returns a different secret state for a different user", () => { + const strategy = new TestForwarder(encryptService, keyService, stateProvider); + + const firstResult = strategy.durableState(SomeUser); + const secondResult = strategy.durableState(AnotherUser); + + expect(firstResult).not.toBe(secondResult); + }); + }); + + it("evaluator returns the default policy evaluator", () => { + const strategy = new TestForwarder(null, null, null); + + const result = strategy.evaluator(null); + + expect(result).toBeInstanceOf(DefaultPolicyEvaluator); + }); +}); diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts new file mode 100644 index 0000000000..554bbfca62 --- /dev/null +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -0,0 +1,73 @@ +import { PolicyType } from "../../../admin-console/enums"; +import { Policy } from "../../../admin-console/models/domain/policy"; +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { KeyDefinition, StateProvider } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { GeneratorStrategy } from "../abstractions"; +import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { NoPolicy } from "../no-policy"; +import { PaddedDataPacker } from "../state/padded-data-packer"; +import { SecretClassifier } from "../state/secret-classifier"; +import { SecretState } from "../state/secret-state"; +import { UserKeyEncryptor } from "../state/user-key-encryptor"; + +import { ApiOptions } from "./options/forwarder-options"; + +const ONE_MINUTE = 60 * 1000; +const OPTIONS_FRAME_SIZE = 512; + +/** An email forwarding service configurable through an API. */ +export abstract class ForwarderGeneratorStrategy< + Options extends ApiOptions, +> extends GeneratorStrategy { + /** Initializes the generator strategy + * @param encryptService protects sensitive forwarder options + * @param keyService looks up the user key when protecting data. + * @param stateProvider creates the durable state for options storage + */ + constructor( + private readonly encryptService: EncryptService, + private readonly keyService: CryptoService, + private stateProvider: StateProvider, + ) { + super(); + // Uses password generator since there aren't policies + // specific to usernames. + this.policy = PolicyType.PasswordGenerator; + + this.cache_ms = ONE_MINUTE; + } + + private durableStates = new Map>>(); + + /** {@link GeneratorStrategy.durableState} */ + durableState = (userId: UserId) => { + let state = this.durableStates.get(userId); + + if (!state) { + const encryptor = this.createEncryptor(); + state = SecretState.from(userId, this.key, this.stateProvider, encryptor); + this.durableStates.set(userId, state); + } + + return state; + }; + + private createEncryptor() { + // always exclude request properties + const classifier = SecretClassifier.allSecret().exclude("website"); + + // construct the encryptor + const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE); + return new UserKeyEncryptor(this.encryptService, this.keyService, classifier, packer); + } + + /** Determine where forwarder configuration is stored */ + protected abstract readonly key: KeyDefinition; + + /** {@link GeneratorStrategy.evaluator} */ + evaluator = (_policy: Policy) => { + return new DefaultPolicyEvaluator(); + }; +} diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts index cc742dc920..c2428aefca 100644 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts @@ -2,22 +2,30 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { ADDY_IO_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; import { AddyIoForwarder } from "./addy-io"; import { mockApiService, mockI18nService } from "./mocks.jest"; describe("Addy.io Forwarder", () => { + it("key returns the Addy IO forwarder key", () => { + const forwarder = new AddyIoForwarder(null, null, null, null, null); + + expect(forwarder.key).toBe(ADDY_IO_FORWARDER); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new AddyIoForwarder(apiService, i18nService); + const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token, domain: "example.com", baseUrl: "https://api.example.com", @@ -34,11 +42,12 @@ describe("Addy.io Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new AddyIoForwarder(apiService, i18nService); + const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain, baseUrl: "https://api.example.com", @@ -56,11 +65,12 @@ describe("Addy.io Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new AddyIoForwarder(apiService, i18nService); + const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", baseUrl, @@ -83,9 +93,10 @@ describe("Addy.io Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new AddyIoForwarder(apiService, i18nService); + const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - await forwarder.generate(website, { + await forwarder.generate({ + website, token: "token", domain: "example.com", baseUrl: "https://api.example.com", @@ -107,9 +118,10 @@ describe("Addy.io Forwarder", () => { const apiService = mockApiService(status, { data: { email } }); const i18nService = mockI18nService(); - const forwarder = new AddyIoForwarder(apiService, i18nService); + const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - const result = await forwarder.generate(null, { + const result = await forwarder.generate({ + website: null, token: "token", domain: "example.com", baseUrl: "https://api.example.com", @@ -124,11 +136,12 @@ describe("Addy.io Forwarder", () => { const apiService = mockApiService(401, {}); const i18nService = mockI18nService(); - const forwarder = new AddyIoForwarder(apiService, i18nService); + const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", baseUrl: "https://api.example.com", @@ -148,11 +161,12 @@ describe("Addy.io Forwarder", () => { const apiService = mockApiService(500, {}); const i18nService = mockI18nService(); - const forwarder = new AddyIoForwarder(apiService, i18nService); + const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", baseUrl: "https://api.example.com", @@ -181,11 +195,12 @@ describe("Addy.io Forwarder", () => { const apiService = mockApiService(statusCode, {}, statusText); const i18nService = mockI18nService(); - const forwarder = new AddyIoForwarder(apiService, i18nService); + const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", baseUrl: "https://api.example.com", diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.ts index 96e02033fe..2db69e2396 100644 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.ts +++ b/libs/common/src/tools/generator/username/forwarders/addy-io.ts @@ -1,24 +1,41 @@ import { ApiService } from "../../../../abstractions/api.service"; +import { CryptoService } from "../../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; +import { StateProvider } from "../../../../platform/state"; +import { ADDY_IO_FORWARDER } from "../../key-definitions"; +import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; -import { EmailDomainOptions, Forwarder, SelfHostedApiOptions } from "../options/forwarder-options"; +import { EmailDomainOptions, SelfHostedApiOptions } from "../options/forwarder-options"; /** Generates a forwarding address for addy.io (formerly anon addy) */ -export class AddyIoForwarder implements Forwarder { +export class AddyIoForwarder extends ForwarderGeneratorStrategy< + SelfHostedApiOptions & EmailDomainOptions +> { /** Instantiates the forwarder * @param apiService used for ajax requests to the forwarding service * @param i18nService used to look up error strings + * @param encryptService protects sensitive forwarder options + * @param keyService looks up the user key when protecting data. + * @param stateProvider creates the durable state for options storage */ constructor( private apiService: ApiService, private i18nService: I18nService, - ) {} + encryptService: EncryptService, + keyService: CryptoService, + stateProvider: StateProvider, + ) { + super(encryptService, keyService, stateProvider); + } - /** {@link Forwarder.generate} */ - async generate( - website: string | null, - options: SelfHostedApiOptions & EmailDomainOptions, - ): Promise { + /** {@link ForwarderGeneratorStrategy.key} */ + get key() { + return ADDY_IO_FORWARDER; + } + + /** {@link ForwarderGeneratorStrategy.generate} */ + generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name); throw error; @@ -32,9 +49,11 @@ export class AddyIoForwarder implements Forwarder { throw error; } - const descriptionId = - website && website !== "" ? "forwarderGeneratedByWithWebsite" : "forwarderGeneratedBy"; - const description = this.i18nService.t(descriptionId, website ?? ""); + let descriptionId = "forwarderGeneratedByWithWebsite"; + if (!options.website || options.website === "") { + descriptionId = "forwarderGeneratedBy"; + } + const description = this.i18nService.t(descriptionId, options.website ?? ""); const url = options.baseUrl + "/api/v1/aliases"; const request = new Request(url, { @@ -70,5 +89,5 @@ export class AddyIoForwarder implements Forwarder { const error = this.i18nService.t("forwarderUnknownError", Forwarders.AddyIo.name); throw error; } - } + }; } diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts index 7b5765f9a7..211eaead6d 100644 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts @@ -2,22 +2,30 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; import { DuckDuckGoForwarder } from "./duck-duck-go"; import { mockApiService, mockI18nService } from "./mocks.jest"; describe("DuckDuckGo Forwarder", () => { + it("key returns the Duck Duck Go forwarder key", () => { + const forwarder = new DuckDuckGoForwarder(null, null, null, null, null); + + expect(forwarder.key).toBe(DUCK_DUCK_GO_FORWARDER); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new DuckDuckGoForwarder(apiService, i18nService); + const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token, }), ).rejects.toEqual("forwaderInvalidToken"); @@ -40,9 +48,10 @@ describe("DuckDuckGo Forwarder", () => { const apiService = mockApiService(status, { address }); const i18nService = mockI18nService(); - const forwarder = new DuckDuckGoForwarder(apiService, i18nService); + const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); - const result = await forwarder.generate(null, { + const result = await forwarder.generate({ + website: null, token: "token", }); @@ -55,11 +64,12 @@ describe("DuckDuckGo Forwarder", () => { const apiService = mockApiService(401, {}); const i18nService = mockI18nService(); - const forwarder = new DuckDuckGoForwarder(apiService, i18nService); + const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", }), ).rejects.toEqual("forwaderInvalidToken"); @@ -76,11 +86,12 @@ describe("DuckDuckGo Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new DuckDuckGoForwarder(apiService, i18nService); + const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", }), ).rejects.toEqual("forwarderUnknownError"); @@ -99,11 +110,12 @@ describe("DuckDuckGo Forwarder", () => { const apiService = mockApiService(statusCode, {}); const i18nService = mockI18nService(); - const forwarder = new DuckDuckGoForwarder(apiService, i18nService); + const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", }), ).rejects.toEqual("forwarderUnknownError"); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts index 8078230b3a..daf4f7b444 100644 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts @@ -1,21 +1,39 @@ import { ApiService } from "../../../../abstractions/api.service"; +import { CryptoService } from "../../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; +import { StateProvider } from "../../../../platform/state"; +import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions"; +import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; -import { ApiOptions, Forwarder } from "../options/forwarder-options"; +import { ApiOptions } from "../options/forwarder-options"; /** Generates a forwarding address for DuckDuckGo */ -export class DuckDuckGoForwarder implements Forwarder { +export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy { /** Instantiates the forwarder * @param apiService used for ajax requests to the forwarding service * @param i18nService used to look up error strings + * @param encryptService protects sensitive forwarder options + * @param keyService looks up the user key when protecting data. + * @param stateProvider creates the durable state for options storage */ constructor( private apiService: ApiService, private i18nService: I18nService, - ) {} + encryptService: EncryptService, + keyService: CryptoService, + stateProvider: StateProvider, + ) { + super(encryptService, keyService, stateProvider); + } - /** {@link Forwarder.generate} */ - async generate(_website: string | null, options: ApiOptions): Promise { + /** {@link ForwarderGeneratorStrategy.key} */ + get key() { + return DUCK_DUCK_GO_FORWARDER; + } + + /** {@link ForwarderGeneratorStrategy.generate} */ + generate = async (options: ApiOptions): Promise => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name); throw error; @@ -48,5 +66,5 @@ export class DuckDuckGoForwarder implements Forwarder { const error = this.i18nService.t("forwarderUnknownError", Forwarders.DuckDuckGo.name); throw error; } - } + }; } diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts index 6d557399aa..bab2b93966 100644 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts @@ -3,6 +3,7 @@ * @jest-environment ../../../../shared/test.environment.ts */ import { ApiService } from "../../../../abstractions/api.service"; +import { FASTMAIL_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; import { FastmailForwarder } from "./fastmail"; @@ -45,16 +46,23 @@ const AccountIdSuccess: MockResponse = Object.freeze({ // the tests describe("Fastmail Forwarder", () => { + it("key returns the Fastmail forwarder key", () => { + const forwarder = new FastmailForwarder(null, null, null, null, null); + + expect(forwarder.key).toBe(FASTMAIL_FORWARDER); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(AccountIdSuccess, EmptyResponse); const i18nService = mockI18nService(); - const forwarder = new FastmailForwarder(apiService, i18nService); + const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token, domain: "example.com", prefix: "prefix", @@ -71,11 +79,12 @@ describe("Fastmail Forwarder", () => { const apiService = mockApiService({ status, body: {} }, EmptyResponse); const i18nService = mockI18nService(); - const forwarder = new FastmailForwarder(apiService, i18nService); + const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", prefix: "prefix", @@ -105,9 +114,10 @@ describe("Fastmail Forwarder", () => { }); const i18nService = mockI18nService(); - const forwarder = new FastmailForwarder(apiService, i18nService); + const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); - const result = await forwarder.generate(null, { + const result = await forwarder.generate({ + website: null, token: "token", domain: "example.com", prefix: "prefix", @@ -138,11 +148,12 @@ describe("Fastmail Forwarder", () => { }); const i18nService = mockI18nService(); - const forwarder = new FastmailForwarder(apiService, i18nService); + const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", prefix: "prefix", @@ -165,11 +176,12 @@ describe("Fastmail Forwarder", () => { const apiService = mockApiService(AccountIdSuccess, { status, body: {} }); const i18nService = mockI18nService(); - const forwarder = new FastmailForwarder(apiService, i18nService); + const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", prefix: "prefix", @@ -206,11 +218,12 @@ describe("Fastmail Forwarder", () => { }); const i18nService = mockI18nService(); - const forwarder = new FastmailForwarder(apiService, i18nService); + const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", prefix: "prefix", @@ -232,11 +245,12 @@ describe("Fastmail Forwarder", () => { const apiService = mockApiService(AccountIdSuccess, { status: statusCode, body: {} }); const i18nService = mockI18nService(); - const forwarder = new FastmailForwarder(apiService, i18nService); + const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", prefix: "prefix", diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.ts index b6b40946a8..b4e2b56695 100644 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.ts +++ b/libs/common/src/tools/generator/username/forwarders/fastmail.ts @@ -1,24 +1,39 @@ import { ApiService } from "../../../../abstractions/api.service"; +import { CryptoService } from "../../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; +import { StateProvider } from "../../../../platform/state"; +import { FASTMAIL_FORWARDER } from "../../key-definitions"; +import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; -import { EmailPrefixOptions, Forwarder, ApiOptions } from "../options/forwarder-options"; +import { EmailPrefixOptions, ApiOptions } from "../options/forwarder-options"; /** Generates a forwarding address for Fastmail */ -export class FastmailForwarder implements Forwarder { +export class FastmailForwarder extends ForwarderGeneratorStrategy { /** Instantiates the forwarder * @param apiService used for ajax requests to the forwarding service * @param i18nService used to look up error strings + * @param encryptService protects sensitive forwarder options + * @param keyService looks up the user key when protecting data. + * @param stateProvider creates the durable state for options storage */ constructor( private apiService: ApiService, private i18nService: I18nService, - ) {} + encryptService: EncryptService, + keyService: CryptoService, + stateProvider: StateProvider, + ) { + super(encryptService, keyService, stateProvider); + } - /** {@link Forwarder.generate} */ - async generate( - website: string | null, - options: ApiOptions & EmailPrefixOptions, - ): Promise { + /** {@link ForwarderGeneratorStrategy.key} */ + get key() { + return FASTMAIL_FORWARDER; + } + + /** {@link ForwarderGeneratorStrategy.generate} */ + generate = async (options: ApiOptions & EmailPrefixOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.Fastmail.name); throw error; @@ -41,7 +56,7 @@ export class FastmailForwarder implements Forwarder { "new-masked-email": { state: "enabled", description: "", - forDomain: website, + forDomain: options.website, emailPrefix: options.prefix, }, }, @@ -104,7 +119,7 @@ export class FastmailForwarder implements Forwarder { const error = this.i18nService.t("forwarderUnknownError", Forwarders.Fastmail.name); throw error; - } + }; private async getAccountId(options: ApiOptions): Promise { const requestInit: RequestInit = { diff --git a/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts b/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts index b5c1c66d6d..5ba8d3f2f1 100644 --- a/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts @@ -2,22 +2,30 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; import { FirefoxRelayForwarder } from "./firefox-relay"; import { mockApiService, mockI18nService } from "./mocks.jest"; describe("Firefox Relay Forwarder", () => { + it("key returns the Firefox Relay forwarder key", () => { + const forwarder = new FirefoxRelayForwarder(null, null, null, null, null); + + expect(forwarder.key).toBe(FIREFOX_RELAY_FORWARDER); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new FirefoxRelayForwarder(apiService, i18nService); + const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token, }), ).rejects.toEqual("forwaderInvalidToken"); @@ -40,9 +48,10 @@ describe("Firefox Relay Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new FirefoxRelayForwarder(apiService, i18nService); + const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); - await forwarder.generate(website, { + await forwarder.generate({ + website, token: "token", }); @@ -62,9 +71,10 @@ describe("Firefox Relay Forwarder", () => { const apiService = mockApiService(status, { full_address }); const i18nService = mockI18nService(); - const forwarder = new FirefoxRelayForwarder(apiService, i18nService); + const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); - const result = await forwarder.generate(null, { + const result = await forwarder.generate({ + website: null, token: "token", }); @@ -77,11 +87,12 @@ describe("Firefox Relay Forwarder", () => { const apiService = mockApiService(401, {}); const i18nService = mockI18nService(); - const forwarder = new FirefoxRelayForwarder(apiService, i18nService); + const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", }), ).rejects.toEqual("forwaderInvalidToken"); @@ -101,11 +112,12 @@ describe("Firefox Relay Forwarder", () => { const apiService = mockApiService(statusCode, {}); const i18nService = mockI18nService(); - const forwarder = new FirefoxRelayForwarder(apiService, i18nService); + const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", }), ).rejects.toEqual("forwarderUnknownError"); diff --git a/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts b/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts index 1bb19ed7fe..1308852224 100644 --- a/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts +++ b/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts @@ -1,21 +1,39 @@ import { ApiService } from "../../../../abstractions/api.service"; +import { CryptoService } from "../../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; +import { StateProvider } from "../../../../platform/state"; +import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions"; +import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; -import { Forwarder, ApiOptions } from "../options/forwarder-options"; +import { ApiOptions } from "../options/forwarder-options"; /** Generates a forwarding address for Firefox Relay */ -export class FirefoxRelayForwarder implements Forwarder { +export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy { /** Instantiates the forwarder * @param apiService used for ajax requests to the forwarding service * @param i18nService used to look up error strings + * @param encryptService protects sensitive forwarder options + * @param keyService looks up the user key when protecting data. + * @param stateProvider creates the durable state for options storage */ constructor( private apiService: ApiService, private i18nService: I18nService, - ) {} + encryptService: EncryptService, + keyService: CryptoService, + stateProvider: StateProvider, + ) { + super(encryptService, keyService, stateProvider); + } - /** {@link Forwarder.generate} */ - async generate(website: string | null, options: ApiOptions): Promise { + /** {@link ForwarderGeneratorStrategy.key} */ + get key() { + return FIREFOX_RELAY_FORWARDER; + } + + /** {@link ForwarderGeneratorStrategy.generate} */ + generate = async (options: ApiOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.FirefoxRelay.name); throw error; @@ -23,9 +41,11 @@ export class FirefoxRelayForwarder implements Forwarder { const url = "https://relay.firefox.com/api/v1/relayaddresses/"; - const descriptionId = - website && website !== "" ? "forwarderGeneratedByWithWebsite" : "forwarderGeneratedBy"; - const description = this.i18nService.t(descriptionId, website ?? ""); + let descriptionId = "forwarderGeneratedByWithWebsite"; + if (!options.website || options.website === "") { + descriptionId = "forwarderGeneratedBy"; + } + const description = this.i18nService.t(descriptionId, options.website ?? ""); const request = new Request(url, { redirect: "manual", @@ -37,7 +57,7 @@ export class FirefoxRelayForwarder implements Forwarder { }), body: JSON.stringify({ enabled: true, - generated_for: website, + generated_for: options.website, description, }), }); @@ -53,5 +73,5 @@ export class FirefoxRelayForwarder implements Forwarder { const error = this.i18nService.t("forwarderUnknownError", Forwarders.FirefoxRelay.name); throw error; } - } + }; } diff --git a/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts b/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts index a5c2e14d37..daf0f3d7f1 100644 --- a/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts @@ -2,22 +2,30 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; import { ForwardEmailForwarder } from "./forward-email"; import { mockApiService, mockI18nService } from "./mocks.jest"; describe("ForwardEmail Forwarder", () => { + it("key returns the Forward Email forwarder key", () => { + const forwarder = new ForwardEmailForwarder(null, null, null, null, null); + + expect(forwarder.key).toBe(FORWARD_EMAIL_FORWARDER); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token, domain: "example.com", }), @@ -36,11 +44,12 @@ describe("ForwardEmail Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain, }), @@ -65,9 +74,10 @@ describe("ForwardEmail Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - await forwarder.generate(website, { + await forwarder.generate({ + website, token: "token", domain: "example.com", }); @@ -92,9 +102,10 @@ describe("ForwardEmail Forwarder", () => { const apiService = mockApiService(status, response); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - const result = await forwarder.generate(null, { + const result = await forwarder.generate({ + website: null, token: "token", domain: "example.com", }); @@ -108,11 +119,12 @@ describe("ForwardEmail Forwarder", () => { const apiService = mockApiService(401, {}); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", }), @@ -132,11 +144,12 @@ describe("ForwardEmail Forwarder", () => { const apiService = mockApiService(401, { message: "A message" }); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", }), @@ -158,11 +171,12 @@ describe("ForwardEmail Forwarder", () => { const apiService = mockApiService(500, json); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", }), @@ -191,11 +205,12 @@ describe("ForwardEmail Forwarder", () => { const apiService = mockApiService(statusCode, { message }); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", }), @@ -225,11 +240,12 @@ describe("ForwardEmail Forwarder", () => { const apiService = mockApiService(statusCode, { error }); const i18nService = mockI18nService(); - const forwarder = new ForwardEmailForwarder(apiService, i18nService); + const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", domain: "example.com", }), diff --git a/libs/common/src/tools/generator/username/forwarders/forward-email.ts b/libs/common/src/tools/generator/username/forwarders/forward-email.ts index 7fc727e471..eb6e3cd0c6 100644 --- a/libs/common/src/tools/generator/username/forwarders/forward-email.ts +++ b/libs/common/src/tools/generator/username/forwarders/forward-email.ts @@ -1,25 +1,42 @@ import { ApiService } from "../../../../abstractions/api.service"; +import { CryptoService } from "../../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { Utils } from "../../../../platform/misc/utils"; +import { StateProvider } from "../../../../platform/state"; +import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions"; +import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; -import { EmailDomainOptions, Forwarder, ApiOptions } from "../options/forwarder-options"; +import { EmailDomainOptions, ApiOptions } from "../options/forwarder-options"; /** Generates a forwarding address for Forward Email */ -export class ForwardEmailForwarder implements Forwarder { +export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< + ApiOptions & EmailDomainOptions +> { /** Instantiates the forwarder * @param apiService used for ajax requests to the forwarding service * @param i18nService used to look up error strings + * @param encryptService protects sensitive forwarder options + * @param keyService looks up the user key when protecting data. + * @param stateProvider creates the durable state for options storage */ constructor( private apiService: ApiService, private i18nService: I18nService, - ) {} + encryptService: EncryptService, + keyService: CryptoService, + stateProvider: StateProvider, + ) { + super(encryptService, keyService, stateProvider); + } - /** {@link Forwarder.generate} */ - async generate( - website: string | null, - options: ApiOptions & EmailDomainOptions, - ): Promise { + /** {@link ForwarderGeneratorStrategy.key} */ + get key() { + return FORWARD_EMAIL_FORWARDER; + } + + /** {@link ForwarderGeneratorStrategy.generate} */ + generate = async (options: ApiOptions & EmailDomainOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.ForwardEmail.name); throw error; @@ -31,9 +48,11 @@ export class ForwardEmailForwarder implements Forwarder { const url = `https://api.forwardemail.net/v1/domains/${options.domain}/aliases`; - const descriptionId = - website && website !== "" ? "forwarderGeneratedByWithWebsite" : "forwarderGeneratedBy"; - const description = this.i18nService.t(descriptionId, website ?? ""); + let descriptionId = "forwarderGeneratedByWithWebsite"; + if (!options.website || options.website === "") { + descriptionId = "forwarderGeneratedBy"; + } + const description = this.i18nService.t(descriptionId, options.website ?? ""); const request = new Request(url, { redirect: "manual", @@ -44,7 +63,7 @@ export class ForwardEmailForwarder implements Forwarder { "Content-Type": "application/json", }), body: JSON.stringify({ - labels: website, + labels: options.website, description, }), }); @@ -75,5 +94,5 @@ export class ForwardEmailForwarder implements Forwarder { const error = this.i18nService.t("forwarderUnknownError", Forwarders.ForwardEmail.name); throw error; } - } + }; } diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts index 60ca329e8b..1120d49ce3 100644 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts @@ -2,22 +2,30 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; import { mockApiService, mockI18nService } from "./mocks.jest"; import { SimpleLoginForwarder } from "./simple-login"; describe("SimpleLogin Forwarder", () => { + it("key returns the Simple Login forwarder key", () => { + const forwarder = new SimpleLoginForwarder(null, null, null, null, null); + + expect(forwarder.key).toBe(SIMPLE_LOGIN_FORWARDER); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new SimpleLoginForwarder(apiService, i18nService); + const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token, baseUrl: "https://api.example.com", }), @@ -36,11 +44,12 @@ describe("SimpleLogin Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new SimpleLoginForwarder(apiService, i18nService); + const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", baseUrl, }), @@ -62,9 +71,10 @@ describe("SimpleLogin Forwarder", () => { const apiService = mockApiService(200, {}); const i18nService = mockI18nService(); - const forwarder = new SimpleLoginForwarder(apiService, i18nService); + const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - await forwarder.generate(website, { + await forwarder.generate({ + website, token: "token", baseUrl: "https://api.example.com", }); @@ -85,9 +95,10 @@ describe("SimpleLogin Forwarder", () => { const apiService = mockApiService(status, { alias }); const i18nService = mockI18nService(); - const forwarder = new SimpleLoginForwarder(apiService, i18nService); + const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - const result = await forwarder.generate(null, { + const result = await forwarder.generate({ + website: null, token: "token", baseUrl: "https://api.example.com", }); @@ -101,11 +112,12 @@ describe("SimpleLogin Forwarder", () => { const apiService = mockApiService(401, {}); const i18nService = mockI18nService(); - const forwarder = new SimpleLoginForwarder(apiService, i18nService); + const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", baseUrl: "https://api.example.com", }), @@ -126,11 +138,12 @@ describe("SimpleLogin Forwarder", () => { const apiService = mockApiService(500, body); const i18nService = mockI18nService(); - const forwarder = new SimpleLoginForwarder(apiService, i18nService); + const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", baseUrl: "https://api.example.com", }), @@ -159,11 +172,12 @@ describe("SimpleLogin Forwarder", () => { const apiService = mockApiService(statusCode, { error }); const i18nService = mockI18nService(); - const forwarder = new SimpleLoginForwarder(apiService, i18nService); + const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); await expect( async () => - await forwarder.generate(null, { + await forwarder.generate({ + website: null, token: "token", baseUrl: "https://api.example.com", }), diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.ts index 0000b680e3..33bd8e3d4e 100644 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.ts +++ b/libs/common/src/tools/generator/username/forwarders/simple-login.ts @@ -1,21 +1,39 @@ import { ApiService } from "../../../../abstractions/api.service"; +import { CryptoService } from "../../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; +import { StateProvider } from "../../../../platform/state"; +import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions"; +import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; -import { Forwarder, SelfHostedApiOptions } from "../options/forwarder-options"; +import { SelfHostedApiOptions } from "../options/forwarder-options"; /** Generates a forwarding address for Simple Login */ -export class SimpleLoginForwarder implements Forwarder { +export class SimpleLoginForwarder extends ForwarderGeneratorStrategy { /** Instantiates the forwarder * @param apiService used for ajax requests to the forwarding service * @param i18nService used to look up error strings + * @param encryptService protects sensitive forwarder options + * @param keyService looks up the user key when protecting data. + * @param stateProvider creates the durable state for options storage */ constructor( private apiService: ApiService, private i18nService: I18nService, - ) {} + encryptService: EncryptService, + keyService: CryptoService, + stateProvider: StateProvider, + ) { + super(encryptService, keyService, stateProvider); + } - /** {@link Forwarder.generate} */ - async generate(website: string, options: SelfHostedApiOptions): Promise { + /** {@link ForwarderGeneratorStrategy.key} */ + get key() { + return SIMPLE_LOGIN_FORWARDER; + } + + /** {@link ForwarderGeneratorStrategy.generate} */ + generate = async (options: SelfHostedApiOptions) => { if (!options.token || options.token === "") { const error = this.i18nService.t("forwaderInvalidToken", Forwarders.SimpleLogin.name); throw error; @@ -27,11 +45,11 @@ export class SimpleLoginForwarder implements Forwarder { let url = options.baseUrl + "/api/alias/random/new"; let noteId = "forwarderGeneratedBy"; - if (website && website !== "") { - url += "?hostname=" + website; + if (options.website && options.website !== "") { + url += "?hostname=" + options.website; noteId = "forwarderGeneratedByWithWebsite"; } - const note = this.i18nService.t(noteId, website ?? ""); + const note = this.i18nService.t(noteId, options.website ?? ""); const request = new Request(url, { redirect: "manual", @@ -60,5 +78,5 @@ export class SimpleLoginForwarder implements Forwarder { const error = this.i18nService.t("forwarderUnknownError", Forwarders.SimpleLogin.name); throw error; } - } + }; } diff --git a/libs/common/src/tools/generator/username/options/constants.ts b/libs/common/src/tools/generator/username/options/constants.ts index 66f5d83af9..8f7013d48b 100644 --- a/libs/common/src/tools/generator/username/options/constants.ts +++ b/libs/common/src/tools/generator/username/options/constants.ts @@ -85,27 +85,33 @@ export const DefaultOptions: UsernameGeneratorOptions = Object.freeze({ forwarders: Object.freeze({ service: Forwarders.Fastmail.id, fastMail: Object.freeze({ + website: null, domain: "", prefix: "", token: "", }), addyIo: Object.freeze({ + website: null, baseUrl: "https://app.addy.io", domain: "", token: "", }), forwardEmail: Object.freeze({ + website: null, token: "", domain: "", }), simpleLogin: Object.freeze({ + website: null, baseUrl: "https://app.simplelogin.io", token: "", }), duckDuckGo: Object.freeze({ + website: null, token: "", }), firefoxRelay: Object.freeze({ + website: null, token: "", }), }), diff --git a/libs/common/src/tools/generator/username/options/forwarder-options.ts b/libs/common/src/tools/generator/username/options/forwarder-options.ts index 02375726c8..f36a58a0db 100644 --- a/libs/common/src/tools/generator/username/options/forwarder-options.ts +++ b/libs/common/src/tools/generator/username/options/forwarder-options.ts @@ -1,5 +1,3 @@ -import { EncString } from "../../../../platform/models/domain/enc-string"; - /** Identifiers for email forwarding services. * @remarks These are used to select forwarder-specific options. * The must be kept in sync with the forwarder implementations. @@ -24,26 +22,24 @@ export type ForwarderMetadata = { validForSelfHosted: boolean; }; -/** An email forwarding service configurable through an API. */ -export interface Forwarder { - /** Generate a forwarding email. - * @param website The website to generate a username for. - * @param options The options to use when generating the username. - */ - generate(website: string | null, options: ApiOptions): Promise; -} - /** Options common to all forwarder APIs */ export type ApiOptions = { /** bearer token that authenticates bitwarden to the forwarder. * This is required to issue an API request. */ token?: string; +} & RequestOptions; - /** encrypted bearer token that authenticates bitwarden to the forwarder. - * This is used to store the token at rest and must be decoded before use. +/** Options that provide contextual information about the application state + * when a forwarder is invoked. + * @remarks these fields should always be omitted when saving options. + */ +export type RequestOptions = { + /** @param website The domain of the website the generated email is used + * within. This should be set to `null` when the request is not specific + * to any website. */ - encryptedToken?: EncString; + website: string | null; }; /** Api configuration for forwarders that support self-hosted installations. */ diff --git a/libs/common/src/tools/generator/username/options/utilities.spec.ts b/libs/common/src/tools/generator/username/options/utilities.spec.ts index 904ac6dbfc..7ab1d9dcfd 100644 --- a/libs/common/src/tools/generator/username/options/utilities.spec.ts +++ b/libs/common/src/tools/generator/username/options/utilities.spec.ts @@ -24,27 +24,33 @@ const TestOptions: UsernameGeneratorOptions = { forwarders: { service: Forwarders.Fastmail.id, fastMail: { + website: null, domain: "httpbin.com", prefix: "foo", token: "some-token", }, addyIo: { + website: null, baseUrl: "https://app.addy.io", domain: "example.com", token: "some-token", }, forwardEmail: { + website: null, token: "some-token", domain: "example.com", }, simpleLogin: { + website: null, baseUrl: "https://app.simplelogin.io", token: "some-token", }, duckDuckGo: { + website: null, token: "some-token", }, firefoxRelay: { + website: null, token: "some-token", }, },